From 44e49d9451f124dfbf855cb7e4eb42cbce598834 Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 23 Oct 2025 17:44:30 +0200 Subject: [PATCH 01/35] feat(tx-cache): Pagination types and impl wip --- crates/tx-cache/src/client.rs | 62 +++++++++-- crates/tx-cache/src/types.rs | 193 ++++++++++++++++++++++++++++++++-- 2 files changed, 236 insertions(+), 19 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 80c77bee..260404aa 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -1,6 +1,6 @@ use crate::types::{ - TxCacheOrdersResponse, TxCacheSendBundleResponse, TxCacheSendTransactionResponse, - TxCacheTransactionsResponse, + PaginationParams, TxCacheOrdersResponse, TxCacheSendBundleResponse, + TxCacheSendTransactionResponse, TxCacheTransactionsResponse, }; use alloy::consensus::TxEnvelope; use eyre::Error; @@ -114,6 +114,38 @@ impl TxCache { .map_err(Into::into) } + async fn get_inner_with_query( + &self, + join: &'static str, + query: PaginationParams, + ) -> Result + where + T: DeserializeOwned, + { + // Append the path to the URL. + let url = self + .url + .join(join) + .inspect_err(|e| warn!(%e, "Failed to join URL. Not querying transaction cache."))?; + + let mut request = self.client.get(url); + + if let Some(cursor) = query.cursor() { + request = request.query(&[("cursor", cursor)]); + } + if let Some(limit) = query.limit() { + request = request.query(&[("limit", limit)]); + } + + request + .send() + .await + .inspect_err(|e| warn!(%e, "Failed to get object from transaction cache."))? + .json::() + .await + .map_err(Into::into) + } + /// Forwards a raw transaction to the URL. #[instrument(skip_all)] pub async fn forward_raw_transaction( @@ -140,17 +172,27 @@ impl TxCache { /// Get transactions from the URL. #[instrument(skip_all)] - pub async fn get_transactions(&self) -> Result, Error> { - let response: TxCacheTransactionsResponse = - self.get_inner::(TRANSACTIONS).await?; - Ok(response.transactions) + pub async fn get_transactions( + &self, + query: Option, + ) -> Result { + if let Some(query) = query { + self.get_inner_with_query::(TRANSACTIONS, query).await + } else { + self.get_inner::(TRANSACTIONS).await + } } /// Get signed orders from the URL. #[instrument(skip_all)] - pub async fn get_orders(&self) -> Result, Error> { - let response: TxCacheOrdersResponse = - self.get_inner::(ORDERS).await?; - Ok(response.orders) + pub async fn get_orders( + &self, + query: Option, + ) -> Result { + if let Some(query) = query { + self.get_inner_with_query::(ORDERS, query).await + } else { + self.get_inner::(ORDERS).await + } } } diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 0d700b54..8c3def00 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -90,11 +90,13 @@ impl TxCacheBundleResponse { pub struct TxCacheBundlesResponse { /// the list of bundles pub bundles: Vec, + /// The pagination info. + pub pagination: PaginationInfo, } impl From> for TxCacheBundlesResponse { fn from(bundles: Vec) -> Self { - Self { bundles } + Self { bundles, pagination: PaginationInfo::empty() } } } @@ -104,16 +106,22 @@ impl From for Vec { } } +impl From<(Vec, PaginationInfo)> for TxCacheBundlesResponse { + fn from((bundles, pagination): (Vec, PaginationInfo)) -> Self { + Self { bundles, pagination } + } +} + impl TxCacheBundlesResponse { /// Create a new bundle response from a list of bundles. pub const fn new(bundles: Vec) -> Self { - Self { bundles } + Self { bundles, pagination: PaginationInfo::empty() } } /// Create a new bundle response from a list of bundles. #[deprecated = "Use `From::from` instead, `Self::new` in const contexts"] pub const fn from_bundles(bundles: Vec) -> Self { - Self::new(bundles) + Self { bundles, pagination: PaginationInfo::empty() } } /// Convert the bundle response to a list of [`SignetEthBundle`]. @@ -121,6 +129,31 @@ impl TxCacheBundlesResponse { pub fn into_bundles(self) -> Vec { self.bundles } + + /// Check if the response is empty (has no bundles). + pub fn is_empty(&self) -> bool { + self.bundles.is_empty() + } + + /// Check if there is a next page in the response. + pub const fn has_next_page(&self) -> bool { + self.pagination.has_next_page() + } + + /// Get the cursor for the next page. + pub fn next_cursor(&self) -> Option<&str> { + self.pagination.next_cursor() + } + + /// Consume the response and return the next cursor. + pub fn into_next_cursor(self) -> Option { + self.pagination.into_next_cursor() + } + + /// Consume the response and return the parts. + pub fn into_parts(self) -> (Vec, PaginationInfo) { + (self.bundles, self.pagination) + } } /// Represents a response to successfully adding or updating a bundle in the transaction cache. @@ -154,11 +187,13 @@ impl From for uuid::Uuid { pub struct TxCacheTransactionsResponse { /// The list of transactions. pub transactions: Vec, + /// The pagination info. + pub pagination: PaginationInfo, } impl From> for TxCacheTransactionsResponse { fn from(transactions: Vec) -> Self { - Self { transactions } + Self { transactions, pagination: PaginationInfo::empty() } } } @@ -168,16 +203,22 @@ impl From for Vec { } } +impl From<(Vec, PaginationInfo)> for TxCacheTransactionsResponse { + fn from((transactions, pagination): (Vec, PaginationInfo)) -> Self { + Self { transactions, pagination } + } +} + impl TxCacheTransactionsResponse { /// Instantiate a new transaction response from a list of transactions. pub const fn new(transactions: Vec) -> Self { - Self { transactions } + Self { transactions, pagination: PaginationInfo::empty() } } /// Create a new transaction response from a list of transactions. #[deprecated = "Use `From::from` instead, or `Self::new` in const contexts"] pub const fn from_transactions(transactions: Vec) -> Self { - Self::new(transactions) + Self { transactions, pagination: PaginationInfo::empty() } } /// Convert the transaction response to a list of [`TxEnvelope`]. @@ -185,6 +226,31 @@ impl TxCacheTransactionsResponse { pub fn into_transactions(self) -> Vec { self.transactions } + + /// Check if the response is empty (has no transactions). + pub fn is_empty(&self) -> bool { + self.transactions.is_empty() + } + + /// Check if there is a next page in the response. + pub const fn has_next_page(&self) -> bool { + self.pagination.has_next_page() + } + + /// Get the cursor for the next page. + pub fn next_cursor(&self) -> Option<&str> { + self.pagination.next_cursor() + } + + /// Consume the response and return the next cursor. + pub fn into_next_cursor(self) -> Option { + self.pagination.into_next_cursor() + } + + /// Consume the response and return the parts. + pub fn into_parts(self) -> (Vec, PaginationInfo) { + (self.transactions, self.pagination) + } } /// Response from the transaction cache to successfully adding a transaction. @@ -230,11 +296,13 @@ impl TxCacheSendTransactionResponse { pub struct TxCacheOrdersResponse { /// The list of signed orders. pub orders: Vec, + /// The pagination info. + pub pagination: PaginationInfo, } impl From> for TxCacheOrdersResponse { fn from(orders: Vec) -> Self { - Self { orders } + Self { orders, pagination: PaginationInfo::empty() } } } @@ -244,16 +312,22 @@ impl From for Vec { } } +impl From<(Vec, PaginationInfo)> for TxCacheOrdersResponse { + fn from((orders, pagination): (Vec, PaginationInfo)) -> Self { + Self { orders, pagination } + } +} + impl TxCacheOrdersResponse { /// Create a new order response from a list of orders. pub const fn new(orders: Vec) -> Self { - Self { orders } + Self { orders, pagination: PaginationInfo::empty() } } /// Create a new order response from a list of orders. #[deprecated = "Use `From::from` instead, `Self::new` in const contexts"] pub const fn from_orders(orders: Vec) -> Self { - Self { orders } + Self { orders, pagination: PaginationInfo::empty() } } /// Convert the order response to a list of [`SignedOrder`]. @@ -261,4 +335,105 @@ impl TxCacheOrdersResponse { pub fn into_orders(self) -> Vec { self.orders } + + /// Check if there is a next page in the response. + pub const fn has_next_page(&self) -> bool { + self.pagination.has_next_page() + } + + /// Get the cursor for the next page. + pub fn next_cursor(&self) -> Option<&str> { + self.pagination.next_cursor() + } + + /// Consume the response and return the next cursor. + pub fn into_next_cursor(self) -> Option { + self.pagination.into_next_cursor() + } + + /// Consume the response and return the parts. + pub fn into_parts(self) -> (Vec, PaginationInfo) { + (self.orders, self.pagination) + } +} + +/// Represents the pagination information from a transaction cache response. +/// This applies to all GET endpoints that return a list of items. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaginationInfo { + next_cursor: Option, + has_next_page: bool, +} + +impl PaginationInfo { + /// Create a new [`PaginationInfo`]. + pub const fn new(next_cursor: Option, has_next_page: bool) -> Self { + Self { next_cursor, has_next_page } + } + + /// Create an empty [`PaginationInfo`]. + pub const fn empty() -> Self { + Self { next_cursor: None, has_next_page: false } + } + + /// Get the next cursor. + pub fn next_cursor(&self) -> Option<&str> { + self.next_cursor.as_deref() + } + + /// Consume the [`PaginationInfo`] and return the next cursor. + pub fn into_next_cursor(self) -> Option { + self.next_cursor + } + + /// Check if there is a next page in the response. + pub const fn has_next_page(&self) -> bool { + self.has_next_page + } +} + +/// A query for pagination. +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct PaginationParams { + /// The cursor to start from. + cursor: Option, + /// The number of items to return. + limit: Option, +} + +impl PaginationParams { + /// Creates a new instance of [`PaginationParams`]. + pub const fn new(cursor: Option, limit: Option) -> Self { + Self { cursor, limit } + } + + /// Get the cursor to start from. + pub fn cursor(&self) -> Option<&str> { + self.cursor.as_deref() + } + + /// Consumes the [`PaginationParams`] and returns the cursor. + pub fn into_cursor(self) -> Option { + self.cursor + } + + /// Get the number of items to return. + pub const fn limit(&self) -> Option { + self.limit + } + + /// Check if the query has a cursor. + pub const fn has_cursor(&self) -> bool { + self.cursor.is_some() + } + + /// Check if the query has a limit. + pub const fn has_limit(&self) -> bool { + self.limit.is_some() + } + + /// Check if the query is empty (has no cursor and no limit). + pub const fn is_empty(&self) -> bool { + !self.has_cursor() && !self.has_limit() + } } From c024d761a881c7e09fa50fa12026a61b029d2279 Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 23 Oct 2025 18:38:50 +0200 Subject: [PATCH 02/35] chore: from impl for paginationinfo/params --- crates/tx-cache/src/types.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 8c3def00..e1f3bdb2 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -401,6 +401,12 @@ pub struct PaginationParams { limit: Option, } +impl From for PaginationParams { + fn from(info: PaginationInfo) -> Self { + Self { cursor: info.into_next_cursor(), limit: None } + } +} + impl PaginationParams { /// Creates a new instance of [`PaginationParams`]. pub const fn new(cursor: Option, limit: Option) -> Self { From 92c4ded9d5f7a76757b98b0f0fd73b4dfd6b5018 Mon Sep 17 00:00:00 2001 From: evalir Date: Mon, 27 Oct 2025 12:32:25 +0100 Subject: [PATCH 03/35] chore: extra helper fns --- crates/tx-cache/src/types.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index e1f3bdb2..bf3570f6 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -150,6 +150,11 @@ impl TxCacheBundlesResponse { self.pagination.into_next_cursor() } + /// Consume the response and return the pagination info. + pub fn into_pagination_info(self) -> PaginationInfo { + self.pagination + } + /// Consume the response and return the parts. pub fn into_parts(self) -> (Vec, PaginationInfo) { (self.bundles, self.pagination) @@ -247,6 +252,11 @@ impl TxCacheTransactionsResponse { self.pagination.into_next_cursor() } + /// Consume the response and return the pagination info. + pub fn into_pagination_info(self) -> PaginationInfo { + self.pagination + } + /// Consume the response and return the parts. pub fn into_parts(self) -> (Vec, PaginationInfo) { (self.transactions, self.pagination) @@ -351,6 +361,11 @@ impl TxCacheOrdersResponse { self.pagination.into_next_cursor() } + /// Consume the response and return the pagination info. + pub fn into_pagination_info(self) -> PaginationInfo { + self.pagination + } + /// Consume the response and return the parts. pub fn into_parts(self) -> (Vec, PaginationInfo) { (self.orders, self.pagination) @@ -423,6 +438,16 @@ impl PaginationParams { self.cursor } + /// Set the limit for the pagination params. + pub fn with_limit(self, limit: u32) -> Self { + Self { limit: Some(limit), cursor: self.cursor } + } + + /// Set the cursor for the pagination params. + pub fn with_cursor(self, cursor: Option) -> Self { + Self { cursor, limit: self.limit } + } + /// Get the number of items to return. pub const fn limit(&self) -> Option { self.limit From 3303fa2779c1b2313dbd875c424eb15c9d9b475c Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 31 Oct 2025 11:41:43 +0100 Subject: [PATCH 04/35] feat: re-design pagination to contain a CacheResponse --- crates/tx-cache/src/client.rs | 18 +-- crates/tx-cache/src/types.rs | 204 ++++++++++++++++------------------ 2 files changed, 107 insertions(+), 115 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 260404aa..43537359 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -1,5 +1,5 @@ use crate::types::{ - PaginationParams, TxCacheOrdersResponse, TxCacheSendBundleResponse, + CacheResponse, PaginationParams, TxCacheOrdersResponse, TxCacheSendBundleResponse, TxCacheSendTransactionResponse, TxCacheTransactionsResponse, }; use alloy::consensus::TxEnvelope; @@ -175,11 +175,15 @@ impl TxCache { pub async fn get_transactions( &self, query: Option, - ) -> Result { + ) -> Result, Error> { if let Some(query) = query { - self.get_inner_with_query::(TRANSACTIONS, query).await + self.get_inner_with_query::>( + TRANSACTIONS, + query, + ) + .await } else { - self.get_inner::(TRANSACTIONS).await + self.get_inner::>(TRANSACTIONS).await } } @@ -188,11 +192,11 @@ impl TxCache { pub async fn get_orders( &self, query: Option, - ) -> Result { + ) -> Result, Error> { if let Some(query) = query { - self.get_inner_with_query::(ORDERS, query).await + self.get_inner_with_query::>(ORDERS, query).await } else { - self.get_inner::(ORDERS).await + self.get_inner::>(ORDERS).await } } } diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index bf3570f6..8ae277e4 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -4,6 +4,93 @@ use serde::{Deserialize, Serialize}; use signet_bundle::SignetEthBundle; use signet_types::SignedOrder; +/// A response from the transaction cache, containing an item. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CacheResponse { + /// A paginated response, containing the inner item and a pagination info. + Paginated { + /// The actual item. + inner: T, + /// The pagination info. + pagination: PaginationInfo, + }, + /// An unpaginated response, containing the actual item. + Unpaginated { + /// The actual item. + inner: T, + }, +} + +impl CacheResponse { + /// Create a new paginated response from a list of items and a pagination info. + pub const fn paginated(inner: T, pagination: PaginationInfo) -> Self { + Self::Paginated { inner, pagination } + } + + /// Create a new unpaginated response from a list of items. + pub const fn unpaginated(inner: T) -> Self { + Self::Unpaginated { inner } + } + + /// Return a reference to the inner value. + pub const fn inner(&self) -> &T { + match self { + Self::Paginated { inner, .. } => inner, + Self::Unpaginated { inner } => inner, + } + } + + /// Return a mutable reference to the inner value. + pub const fn inner_mut(&mut self) -> &mut T { + match self { + Self::Paginated { inner, .. } => inner, + Self::Unpaginated { inner } => inner, + } + } + + /// Return the pagination info, if any. + pub const fn pagination_info(&self) -> Option<&PaginationInfo> { + match self { + Self::Paginated { pagination, .. } => Some(pagination), + Self::Unpaginated { .. } => None, + } + } + + /// Check if the response is paginated. + pub const fn is_paginated(&self) -> bool { + matches!(self, Self::Paginated { .. }) + } + + /// Check if the response is unpaginated. + pub const fn is_unpaginated(&self) -> bool { + matches!(self, Self::Unpaginated { .. }) + } + + /// Get the inner value. + pub fn into_inner(self) -> T { + match self { + Self::Paginated { inner, .. } => inner, + Self::Unpaginated { inner } => inner, + } + } + + /// Consume the response and return the parts. + pub fn into_parts(self) -> (T, Option) { + match self { + Self::Paginated { inner, pagination } => (inner, Some(pagination)), + Self::Unpaginated { inner } => (inner, None), + } + } + + /// Consume the response and return the pagination info, if any. + pub fn into_pagination_info(self) -> Option { + match self { + Self::Paginated { pagination, .. } => Some(pagination), + Self::Unpaginated { .. } => None, + } + } +} + /// A bundle response from the transaction cache, containing a UUID and a /// [`SignetEthBundle`]. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -90,13 +177,11 @@ impl TxCacheBundleResponse { pub struct TxCacheBundlesResponse { /// the list of bundles pub bundles: Vec, - /// The pagination info. - pub pagination: PaginationInfo, } impl From> for TxCacheBundlesResponse { fn from(bundles: Vec) -> Self { - Self { bundles, pagination: PaginationInfo::empty() } + Self { bundles } } } @@ -106,22 +191,16 @@ impl From for Vec { } } -impl From<(Vec, PaginationInfo)> for TxCacheBundlesResponse { - fn from((bundles, pagination): (Vec, PaginationInfo)) -> Self { - Self { bundles, pagination } - } -} - impl TxCacheBundlesResponse { /// Create a new bundle response from a list of bundles. pub const fn new(bundles: Vec) -> Self { - Self { bundles, pagination: PaginationInfo::empty() } + Self { bundles } } /// Create a new bundle response from a list of bundles. #[deprecated = "Use `From::from` instead, `Self::new` in const contexts"] pub const fn from_bundles(bundles: Vec) -> Self { - Self { bundles, pagination: PaginationInfo::empty() } + Self { bundles } } /// Convert the bundle response to a list of [`SignetEthBundle`]. @@ -134,31 +213,6 @@ impl TxCacheBundlesResponse { pub fn is_empty(&self) -> bool { self.bundles.is_empty() } - - /// Check if there is a next page in the response. - pub const fn has_next_page(&self) -> bool { - self.pagination.has_next_page() - } - - /// Get the cursor for the next page. - pub fn next_cursor(&self) -> Option<&str> { - self.pagination.next_cursor() - } - - /// Consume the response and return the next cursor. - pub fn into_next_cursor(self) -> Option { - self.pagination.into_next_cursor() - } - - /// Consume the response and return the pagination info. - pub fn into_pagination_info(self) -> PaginationInfo { - self.pagination - } - - /// Consume the response and return the parts. - pub fn into_parts(self) -> (Vec, PaginationInfo) { - (self.bundles, self.pagination) - } } /// Represents a response to successfully adding or updating a bundle in the transaction cache. @@ -192,13 +246,11 @@ impl From for uuid::Uuid { pub struct TxCacheTransactionsResponse { /// The list of transactions. pub transactions: Vec, - /// The pagination info. - pub pagination: PaginationInfo, } impl From> for TxCacheTransactionsResponse { fn from(transactions: Vec) -> Self { - Self { transactions, pagination: PaginationInfo::empty() } + Self { transactions } } } @@ -208,22 +260,16 @@ impl From for Vec { } } -impl From<(Vec, PaginationInfo)> for TxCacheTransactionsResponse { - fn from((transactions, pagination): (Vec, PaginationInfo)) -> Self { - Self { transactions, pagination } - } -} - impl TxCacheTransactionsResponse { /// Instantiate a new transaction response from a list of transactions. pub const fn new(transactions: Vec) -> Self { - Self { transactions, pagination: PaginationInfo::empty() } + Self { transactions } } /// Create a new transaction response from a list of transactions. #[deprecated = "Use `From::from` instead, or `Self::new` in const contexts"] pub const fn from_transactions(transactions: Vec) -> Self { - Self { transactions, pagination: PaginationInfo::empty() } + Self { transactions } } /// Convert the transaction response to a list of [`TxEnvelope`]. @@ -236,31 +282,6 @@ impl TxCacheTransactionsResponse { pub fn is_empty(&self) -> bool { self.transactions.is_empty() } - - /// Check if there is a next page in the response. - pub const fn has_next_page(&self) -> bool { - self.pagination.has_next_page() - } - - /// Get the cursor for the next page. - pub fn next_cursor(&self) -> Option<&str> { - self.pagination.next_cursor() - } - - /// Consume the response and return the next cursor. - pub fn into_next_cursor(self) -> Option { - self.pagination.into_next_cursor() - } - - /// Consume the response and return the pagination info. - pub fn into_pagination_info(self) -> PaginationInfo { - self.pagination - } - - /// Consume the response and return the parts. - pub fn into_parts(self) -> (Vec, PaginationInfo) { - (self.transactions, self.pagination) - } } /// Response from the transaction cache to successfully adding a transaction. @@ -306,13 +327,11 @@ impl TxCacheSendTransactionResponse { pub struct TxCacheOrdersResponse { /// The list of signed orders. pub orders: Vec, - /// The pagination info. - pub pagination: PaginationInfo, } impl From> for TxCacheOrdersResponse { fn from(orders: Vec) -> Self { - Self { orders, pagination: PaginationInfo::empty() } + Self { orders } } } @@ -322,22 +341,16 @@ impl From for Vec { } } -impl From<(Vec, PaginationInfo)> for TxCacheOrdersResponse { - fn from((orders, pagination): (Vec, PaginationInfo)) -> Self { - Self { orders, pagination } - } -} - impl TxCacheOrdersResponse { /// Create a new order response from a list of orders. pub const fn new(orders: Vec) -> Self { - Self { orders, pagination: PaginationInfo::empty() } + Self { orders } } /// Create a new order response from a list of orders. #[deprecated = "Use `From::from` instead, `Self::new` in const contexts"] pub const fn from_orders(orders: Vec) -> Self { - Self { orders, pagination: PaginationInfo::empty() } + Self { orders } } /// Convert the order response to a list of [`SignedOrder`]. @@ -345,31 +358,6 @@ impl TxCacheOrdersResponse { pub fn into_orders(self) -> Vec { self.orders } - - /// Check if there is a next page in the response. - pub const fn has_next_page(&self) -> bool { - self.pagination.has_next_page() - } - - /// Get the cursor for the next page. - pub fn next_cursor(&self) -> Option<&str> { - self.pagination.next_cursor() - } - - /// Consume the response and return the next cursor. - pub fn into_next_cursor(self) -> Option { - self.pagination.into_next_cursor() - } - - /// Consume the response and return the pagination info. - pub fn into_pagination_info(self) -> PaginationInfo { - self.pagination - } - - /// Consume the response and return the parts. - pub fn into_parts(self) -> (Vec, PaginationInfo) { - (self.orders, self.pagination) - } } /// Represents the pagination information from a transaction cache response. From af2decb2c45f55a1f926691069b54770fb1864ab Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 6 Nov 2025 15:59:59 +0100 Subject: [PATCH 05/35] chore: adapt client --- crates/tx-cache/src/client.rs | 36 ++++---- crates/tx-cache/src/types.rs | 149 ++++++++++++++++++++++------------ 2 files changed, 115 insertions(+), 70 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 43537359..21cea3d8 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -1,6 +1,6 @@ use crate::types::{ - CacheResponse, PaginationParams, TxCacheOrdersResponse, TxCacheSendBundleResponse, - TxCacheSendTransactionResponse, TxCacheTransactionsResponse, + CacheResponse, CursorKey, OrderKey, PaginationParams, TxCacheOrdersResponse, + TxCacheSendBundleResponse, TxCacheSendTransactionResponse, TxCacheTransactionsResponse, TxKey, }; use alloy::consensus::TxEnvelope; use eyre::Error; @@ -114,10 +114,10 @@ impl TxCache { .map_err(Into::into) } - async fn get_inner_with_query( + async fn get_inner_with_query( &self, join: &'static str, - query: PaginationParams, + query: PaginationParams, ) -> Result where T: DeserializeOwned, @@ -128,14 +128,7 @@ impl TxCache { .join(join) .inspect_err(|e| warn!(%e, "Failed to join URL. Not querying transaction cache."))?; - let mut request = self.client.get(url); - - if let Some(cursor) = query.cursor() { - request = request.query(&[("cursor", cursor)]); - } - if let Some(limit) = query.limit() { - request = request.query(&[("limit", limit)]); - } + let request = self.client.get(url).query(&query.cursor().to_query_object()); request .send() @@ -174,16 +167,16 @@ impl TxCache { #[instrument(skip_all)] pub async fn get_transactions( &self, - query: Option, - ) -> Result, Error> { + query: Option>, + ) -> Result, Error> { if let Some(query) = query { - self.get_inner_with_query::>( + self.get_inner_with_query::>( TRANSACTIONS, query, ) .await } else { - self.get_inner::>(TRANSACTIONS).await + self.get_inner::>(TRANSACTIONS).await } } @@ -191,12 +184,15 @@ impl TxCache { #[instrument(skip_all)] pub async fn get_orders( &self, - query: Option, - ) -> Result, Error> { + query: Option>, + ) -> Result, Error> { if let Some(query) = query { - self.get_inner_with_query::>(ORDERS, query).await + self.get_inner_with_query::>( + ORDERS, query, + ) + .await } else { - self.get_inner::>(ORDERS).await + self.get_inner::>(ORDERS).await } } } diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 8ae277e4..e8d7bd45 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -1,18 +1,21 @@ //! The endpoints for the transaction cache. +use std::collections::HashMap; + use alloy::{consensus::TxEnvelope, primitives::B256}; use serde::{Deserialize, Serialize}; use signet_bundle::SignetEthBundle; use signet_types::SignedOrder; +use uuid::Uuid; /// A response from the transaction cache, containing an item. #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum CacheResponse { +pub enum CacheResponse { /// A paginated response, containing the inner item and a pagination info. Paginated { /// The actual item. inner: T, /// The pagination info. - pagination: PaginationInfo, + pagination: PaginationInfo, }, /// An unpaginated response, containing the actual item. Unpaginated { @@ -21,9 +24,9 @@ pub enum CacheResponse { }, } -impl CacheResponse { +impl CacheResponse { /// Create a new paginated response from a list of items and a pagination info. - pub const fn paginated(inner: T, pagination: PaginationInfo) -> Self { + pub const fn paginated(inner: T, pagination: PaginationInfo) -> Self { Self::Paginated { inner, pagination } } @@ -49,7 +52,7 @@ impl CacheResponse { } /// Return the pagination info, if any. - pub const fn pagination_info(&self) -> Option<&PaginationInfo> { + pub const fn pagination_info(&self) -> Option<&PaginationInfo> { match self { Self::Paginated { pagination, .. } => Some(pagination), Self::Unpaginated { .. } => None, @@ -75,7 +78,7 @@ impl CacheResponse { } /// Consume the response and return the parts. - pub fn into_parts(self) -> (T, Option) { + pub fn into_parts(self) -> (T, Option>) { match self { Self::Paginated { inner, pagination } => (inner, Some(pagination)), Self::Unpaginated { inner } => (inner, None), @@ -83,7 +86,7 @@ impl CacheResponse { } /// Consume the response and return the pagination info, if any. - pub fn into_pagination_info(self) -> Option { + pub fn into_pagination_info(self) -> Option> { match self { Self::Paginated { pagination, .. } => Some(pagination), Self::Unpaginated { .. } => None, @@ -363,14 +366,16 @@ impl TxCacheOrdersResponse { /// Represents the pagination information from a transaction cache response. /// This applies to all GET endpoints that return a list of items. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PaginationInfo { - next_cursor: Option, +pub struct PaginationInfo { + /// The next cursor. + next_cursor: Option, + /// Whether there is a next page. has_next_page: bool, } -impl PaginationInfo { +impl PaginationInfo { /// Create a new [`PaginationInfo`]. - pub const fn new(next_cursor: Option, has_next_page: bool) -> Self { + pub const fn new(next_cursor: Option, has_next_page: bool) -> Self { Self { next_cursor, has_next_page } } @@ -380,12 +385,12 @@ impl PaginationInfo { } /// Get the next cursor. - pub fn next_cursor(&self) -> Option<&str> { - self.next_cursor.as_deref() + pub const fn next_cursor(&self) -> Option<&T> { + self.next_cursor.as_ref() } /// Consume the [`PaginationInfo`] and return the next cursor. - pub fn into_next_cursor(self) -> Option { + pub fn into_next_cursor(self) -> Option { self.next_cursor } @@ -395,34 +400,97 @@ impl PaginationInfo { } } +/// A trait for allowing crusor keys to be converted into an URL query object. +pub trait CursorKey { + /// Convert the cursor key into a URL query object. + fn to_query_object(&self) -> HashMap; +} + +/// The query object keys for the transaction GET endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TxKey { + /// The transaction hash + pub txn_hash: B256, + /// The transaction score + pub score: u64, + /// The global transaction score key + pub global_transaction_score_key: String, +} + +impl CursorKey for TxKey { + fn to_query_object(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("txn_hash".to_string(), self.txn_hash.to_string()); + map.insert("score".to_string(), self.score.to_string()); + map.insert( + "global_transaction_score_key".to_string(), + self.global_transaction_score_key.to_string(), + ); + map + } +} + +/// The query object keys for the bundle GET endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BundleKey { + /// The bundle id + pub id: Uuid, + /// The bundle score + pub score: u64, + /// The global bundle score key + pub global_bundle_score_key: String, +} + +impl CursorKey for BundleKey { + fn to_query_object(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("id".to_string(), self.id.to_string()); + map.insert("score".to_string(), self.score.to_string()); + map.insert("global_bundle_score_key".to_string(), self.global_bundle_score_key.to_string()); + map + } +} + +/// The query object keys for the order GET endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderKey { + /// The order id + pub id: String, +} + +impl CursorKey for OrderKey { + fn to_query_object(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("id".to_string(), self.id.to_string()); + map + } +} + /// A query for pagination. #[derive(Clone, Debug, Serialize, Deserialize, Default)] -pub struct PaginationParams { +pub struct PaginationParams { /// The cursor to start from. - cursor: Option, + cursor: T, /// The number of items to return. + #[serde(skip_serializing_if = "Option::is_none")] limit: Option, } -impl From for PaginationParams { - fn from(info: PaginationInfo) -> Self { - Self { cursor: info.into_next_cursor(), limit: None } - } -} - -impl PaginationParams { +impl PaginationParams { /// Creates a new instance of [`PaginationParams`]. - pub const fn new(cursor: Option, limit: Option) -> Self { + pub const fn new(cursor: T, limit: Option) -> Self { Self { cursor, limit } } /// Get the cursor to start from. - pub fn cursor(&self) -> Option<&str> { - self.cursor.as_deref() + pub const fn cursor(&self) -> &T { + &self.cursor } /// Consumes the [`PaginationParams`] and returns the cursor. - pub fn into_cursor(self) -> Option { + pub fn into_cursor(self) -> T { self.cursor } @@ -430,29 +498,10 @@ impl PaginationParams { pub fn with_limit(self, limit: u32) -> Self { Self { limit: Some(limit), cursor: self.cursor } } +} - /// Set the cursor for the pagination params. - pub fn with_cursor(self, cursor: Option) -> Self { - Self { cursor, limit: self.limit } - } - - /// Get the number of items to return. - pub const fn limit(&self) -> Option { - self.limit - } - - /// Check if the query has a cursor. - pub const fn has_cursor(&self) -> bool { - self.cursor.is_some() - } - - /// Check if the query has a limit. - pub const fn has_limit(&self) -> bool { - self.limit.is_some() - } - - /// Check if the query is empty (has no cursor and no limit). - pub const fn is_empty(&self) -> bool { - !self.has_cursor() && !self.has_limit() +impl CursorKey for PaginationParams { + fn to_query_object(&self) -> HashMap { + self.cursor.to_query_object() } } From e4f385cd922cd38d4eb1c22e7cec118eae9d5578 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 7 Nov 2025 18:35:27 +0100 Subject: [PATCH 06/35] feat: traits for cursor/cache objects --- crates/tx-cache/src/client.rs | 23 ++++++++-------------- crates/tx-cache/src/types.rs | 37 +++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 21cea3d8..94cf3b2c 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -1,5 +1,5 @@ use crate::types::{ - CacheResponse, CursorKey, OrderKey, PaginationParams, TxCacheOrdersResponse, + CacheObject, CacheResponse, CursorKey, OrderKey, PaginationParams, TxCacheOrdersResponse, TxCacheSendBundleResponse, TxCacheSendTransactionResponse, TxCacheTransactionsResponse, TxKey, }; use alloy::consensus::TxEnvelope; @@ -114,13 +114,13 @@ impl TxCache { .map_err(Into::into) } - async fn get_inner_with_query( + async fn get_inner_with_query( &self, join: &'static str, - query: PaginationParams, + query: PaginationParams, ) -> Result where - T: DeserializeOwned, + T: DeserializeOwned + CacheObject, { // Append the path to the URL. let url = self @@ -170,13 +170,9 @@ impl TxCache { query: Option>, ) -> Result, Error> { if let Some(query) = query { - self.get_inner_with_query::>( - TRANSACTIONS, - query, - ) - .await + self.get_inner_with_query(TRANSACTIONS, query).await } else { - self.get_inner::>(TRANSACTIONS).await + self.get_inner(TRANSACTIONS).await } } @@ -187,12 +183,9 @@ impl TxCache { query: Option>, ) -> Result, Error> { if let Some(query) = query { - self.get_inner_with_query::>( - ORDERS, query, - ) - .await + self.get_inner_with_query(ORDERS, query).await } else { - self.get_inner::>(ORDERS).await + self.get_inner(ORDERS).await } } } diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index e8d7bd45..f00a19d1 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -1,12 +1,23 @@ //! The endpoints for the transaction cache. -use std::collections::HashMap; - use alloy::{consensus::TxEnvelope, primitives::B256}; use serde::{Deserialize, Serialize}; use signet_bundle::SignetEthBundle; use signet_types::SignedOrder; +use std::collections::HashMap; use uuid::Uuid; +/// A trait for allowing crusor keys to be converted into an URL query object. +pub trait CursorKey { + /// Convert the cursor key into a URL query object. + fn to_query_object(&self) -> HashMap; +} + +/// A trait for types that can be used as a cache object. +pub trait CacheObject { + /// The cursor key type for the cache object. + type Key: CursorKey; +} + /// A response from the transaction cache, containing an item. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum CacheResponse { @@ -24,6 +35,10 @@ pub enum CacheResponse { }, } +impl CacheObject for CacheResponse { + type Key = C; +} + impl CacheResponse { /// Create a new paginated response from a list of items and a pagination info. pub const fn paginated(inner: T, pagination: PaginationInfo) -> Self { @@ -194,6 +209,10 @@ impl From for Vec { } } +impl CacheObject for TxCacheBundlesResponse { + type Key = BundleKey; +} + impl TxCacheBundlesResponse { /// Create a new bundle response from a list of bundles. pub const fn new(bundles: Vec) -> Self { @@ -263,6 +282,10 @@ impl From for Vec { } } +impl CacheObject for TxCacheTransactionsResponse { + type Key = TxKey; +} + impl TxCacheTransactionsResponse { /// Instantiate a new transaction response from a list of transactions. pub const fn new(transactions: Vec) -> Self { @@ -344,6 +367,10 @@ impl From for Vec { } } +impl CacheObject for TxCacheOrdersResponse { + type Key = OrderKey; +} + impl TxCacheOrdersResponse { /// Create a new order response from a list of orders. pub const fn new(orders: Vec) -> Self { @@ -400,12 +427,6 @@ impl PaginationInfo { } } -/// A trait for allowing crusor keys to be converted into an URL query object. -pub trait CursorKey { - /// Convert the cursor key into a URL query object. - fn to_query_object(&self) -> HashMap; -} - /// The query object keys for the transaction GET endpoint. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] From 09dfe87e29d0f1de61d07e97729e4fd9ae52ac79 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 7 Nov 2025 18:55:56 +0100 Subject: [PATCH 07/35] chore: utility fn --- crates/tx-cache/src/types.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index f00a19d1..07965beb 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -519,6 +519,11 @@ impl PaginationParams { pub fn with_limit(self, limit: u32) -> Self { Self { limit: Some(limit), cursor: self.cursor } } + + /// Get the limit for the items returned. + pub const fn limit(&self) -> Option { + self.limit + } } impl CursorKey for PaginationParams { From 3e7d6678a1b5a4e7a0c8117406439103feb11329 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 7 Nov 2025 19:01:12 +0100 Subject: [PATCH 08/35] chore: make cacheresponse untagged --- crates/tx-cache/src/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 07965beb..19037f89 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -20,6 +20,7 @@ pub trait CacheObject { /// A response from the transaction cache, containing an item. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] pub enum CacheResponse { /// A paginated response, containing the inner item and a pagination info. Paginated { From 3dd9b7803a2d8cb508270dea22b1562ba0622e1e Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 7 Nov 2025 19:02:07 +0100 Subject: [PATCH 09/35] chore: flatten the actual response type on CacheResponse --- crates/tx-cache/src/types.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 19037f89..073387ae 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -25,6 +25,7 @@ pub enum CacheResponse { /// A paginated response, containing the inner item and a pagination info. Paginated { /// The actual item. + #[serde(flatten)] inner: T, /// The pagination info. pagination: PaginationInfo, @@ -32,6 +33,7 @@ pub enum CacheResponse { /// An unpaginated response, containing the actual item. Unpaginated { /// The actual item. + #[serde(flatten)] inner: T, }, } From 44e53a13e01afcf136843ea78ce89636c1682bf6 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 7 Nov 2025 19:15:59 +0100 Subject: [PATCH 10/35] chore: use C for anything cursorkey related --- crates/tx-cache/src/types.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 073387ae..e906f176 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -396,16 +396,16 @@ impl TxCacheOrdersResponse { /// Represents the pagination information from a transaction cache response. /// This applies to all GET endpoints that return a list of items. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PaginationInfo { +pub struct PaginationInfo { /// The next cursor. - next_cursor: Option, + next_cursor: Option, /// Whether there is a next page. has_next_page: bool, } -impl PaginationInfo { +impl PaginationInfo { /// Create a new [`PaginationInfo`]. - pub const fn new(next_cursor: Option, has_next_page: bool) -> Self { + pub const fn new(next_cursor: Option, has_next_page: bool) -> Self { Self { next_cursor, has_next_page } } @@ -415,12 +415,12 @@ impl PaginationInfo { } /// Get the next cursor. - pub const fn next_cursor(&self) -> Option<&T> { + pub const fn next_cursor(&self) -> Option<&C> { self.next_cursor.as_ref() } /// Consume the [`PaginationInfo`] and return the next cursor. - pub fn into_next_cursor(self) -> Option { + pub fn into_next_cursor(self) -> Option { self.next_cursor } @@ -494,27 +494,27 @@ impl CursorKey for OrderKey { /// A query for pagination. #[derive(Clone, Debug, Serialize, Deserialize, Default)] -pub struct PaginationParams { +pub struct PaginationParams { /// The cursor to start from. - cursor: T, + cursor: C, /// The number of items to return. #[serde(skip_serializing_if = "Option::is_none")] limit: Option, } -impl PaginationParams { +impl PaginationParams { /// Creates a new instance of [`PaginationParams`]. - pub const fn new(cursor: T, limit: Option) -> Self { + pub const fn new(cursor: C, limit: Option) -> Self { Self { cursor, limit } } /// Get the cursor to start from. - pub const fn cursor(&self) -> &T { + pub const fn cursor(&self) -> &C { &self.cursor } /// Consumes the [`PaginationParams`] and returns the cursor. - pub fn into_cursor(self) -> T { + pub fn into_cursor(self) -> C { self.cursor } @@ -529,7 +529,7 @@ impl PaginationParams { } } -impl CursorKey for PaginationParams { +impl CursorKey for PaginationParams { fn to_query_object(&self) -> HashMap { self.cursor.to_query_object() } From 404e06270774d7c51007cd67f1ac8c1201a15314 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 7 Nov 2025 21:17:14 +0100 Subject: [PATCH 11/35] chore: more type foo --- crates/tx-cache/src/client.rs | 6 +++--- crates/tx-cache/src/types.rs | 27 ++++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 94cf3b2c..c6ca9fc8 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -108,7 +108,7 @@ impl TxCache { .get(url) .send() .await - .inspect_err(|e| warn!(%e, "Failed to get object from transaction cache"))? + .inspect_err(|e| warn!(%e, "Failed to get object from transaction cache."))? .json::() .await .map_err(Into::into) @@ -168,7 +168,7 @@ impl TxCache { pub async fn get_transactions( &self, query: Option>, - ) -> Result, Error> { + ) -> Result, Error> { if let Some(query) = query { self.get_inner_with_query(TRANSACTIONS, query).await } else { @@ -181,7 +181,7 @@ impl TxCache { pub async fn get_orders( &self, query: Option>, - ) -> Result, Error> { + ) -> Result, Error> { if let Some(query) = query { self.get_inner_with_query(ORDERS, query).await } else { diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index e906f176..bedf4405 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -21,14 +21,17 @@ pub trait CacheObject { /// A response from the transaction cache, containing an item. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] -pub enum CacheResponse { +pub enum CacheResponse +where + T::Key: Serialize + for<'a> Deserialize<'a>, +{ /// A paginated response, containing the inner item and a pagination info. Paginated { /// The actual item. #[serde(flatten)] inner: T, /// The pagination info. - pagination: PaginationInfo, + pagination: PaginationInfo, }, /// An unpaginated response, containing the actual item. Unpaginated { @@ -38,13 +41,19 @@ pub enum CacheResponse { }, } -impl CacheObject for CacheResponse { - type Key = C; +impl CacheObject for CacheResponse +where + T::Key: Serialize + for<'a> Deserialize<'a>, +{ + type Key = T::Key; } -impl CacheResponse { +impl CacheResponse +where + T::Key: Serialize + for<'a> Deserialize<'a>, +{ /// Create a new paginated response from a list of items and a pagination info. - pub const fn paginated(inner: T, pagination: PaginationInfo) -> Self { + pub const fn paginated(inner: T, pagination: PaginationInfo) -> Self { Self::Paginated { inner, pagination } } @@ -70,7 +79,7 @@ impl CacheResponse { } /// Return the pagination info, if any. - pub const fn pagination_info(&self) -> Option<&PaginationInfo> { + pub const fn pagination_info(&self) -> Option<&PaginationInfo> { match self { Self::Paginated { pagination, .. } => Some(pagination), Self::Unpaginated { .. } => None, @@ -96,7 +105,7 @@ impl CacheResponse { } /// Consume the response and return the parts. - pub fn into_parts(self) -> (T, Option>) { + pub fn into_parts(self) -> (T, Option>) { match self { Self::Paginated { inner, pagination } => (inner, Some(pagination)), Self::Unpaginated { inner } => (inner, None), @@ -104,7 +113,7 @@ impl CacheResponse { } /// Consume the response and return the pagination info, if any. - pub fn into_pagination_info(self) -> Option> { + pub fn into_pagination_info(self) -> Option> { match self { Self::Paginated { pagination, .. } => Some(pagination), Self::Unpaginated { .. } => None, From ae4bbccf17e1bdedd642d847ba2820b7236477e4 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 7 Nov 2025 21:21:02 +0100 Subject: [PATCH 12/35] chore: response type for orders --- crates/tx-cache/src/types.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index bedf4405..e137a77d 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -402,6 +402,36 @@ impl TxCacheOrdersResponse { } } +/// Response from the transaction cache to successfully adding an order. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct TxCacheSendOrderResponse { + /// The order id + pub id: B256, +} + +impl From for TxCacheSendOrderResponse { + fn from(id: B256) -> Self { + Self { id } + } +} + +impl From for B256 { + fn from(response: TxCacheSendOrderResponse) -> Self { + response.id + } +} + +impl CacheObject for TxCacheSendOrderResponse { + type Key = OrderKey; +} + +impl TxCacheSendOrderResponse { + /// Create a new order response from an order id. + pub const fn new(id: B256) -> Self { + Self { id } + } +} + /// Represents the pagination information from a transaction cache response. /// This applies to all GET endpoints that return a list of items. #[derive(Debug, Clone, Serialize, Deserialize)] From f494d50b1b38df64b0c57bc9ad1bfa8f50c5a97f Mon Sep 17 00:00:00 2001 From: evalir Date: Mon, 10 Nov 2025 19:47:25 +0100 Subject: [PATCH 13/35] chore: dedup --- crates/tx-cache/Cargo.toml | 2 +- crates/tx-cache/src/client.rs | 45 ++++++++--------------------------- crates/tx-cache/src/types.rs | 12 ++++++++++ 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/crates/tx-cache/Cargo.toml b/crates/tx-cache/Cargo.toml index 2271e666..2aafc1f9 100644 --- a/crates/tx-cache/Cargo.toml +++ b/crates/tx-cache/Cargo.toml @@ -21,4 +21,4 @@ eyre.workspace = true reqwest.workspace = true serde = { workspace = true, features = ["derive"] } tracing.workspace = true -uuid = { workspace = true, features = ["serde"] } \ No newline at end of file +uuid = { workspace = true, features = ["serde", "v4"] } \ No newline at end of file diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index c6ca9fc8..47562413 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -93,43 +93,26 @@ impl TxCache { self.client.post(url).json(&obj).send().await?.error_for_status().map_err(Into::into) } - async fn get_inner(&self, join: &'static str) -> Result - where - T: DeserializeOwned, - { - // Append the path to the URL. - let url = self - .url - .join(join) - .inspect_err(|e| warn!(%e, "Failed to join URL. Not querying transaction cache."))?; - - // Get the result. - self.client - .get(url) - .send() - .await - .inspect_err(|e| warn!(%e, "Failed to get object from transaction cache."))? - .json::() - .await - .map_err(Into::into) - } - - async fn get_inner_with_query( + async fn get_inner( &self, join: &'static str, - query: PaginationParams, + query: Option>, ) -> Result where T: DeserializeOwned + CacheObject, { - // Append the path to the URL. let url = self .url .join(join) .inspect_err(|e| warn!(%e, "Failed to join URL. Not querying transaction cache."))?; - let request = self.client.get(url).query(&query.cursor().to_query_object()); + let request = if let Some(query) = query { + self.client.get(url).query(&query.cursor().to_query_object()) + } else { + self.client.get(url) + }; + // Get the result. request .send() .await @@ -169,11 +152,7 @@ impl TxCache { &self, query: Option>, ) -> Result, Error> { - if let Some(query) = query { - self.get_inner_with_query(TRANSACTIONS, query).await - } else { - self.get_inner(TRANSACTIONS).await - } + self.get_inner(TRANSACTIONS, query).await } /// Get signed orders from the URL. @@ -182,10 +161,6 @@ impl TxCache { &self, query: Option>, ) -> Result, Error> { - if let Some(query) = query { - self.get_inner_with_query(ORDERS, query).await - } else { - self.get_inner(ORDERS).await - } + self.get_inner(ORDERS, query).await } } diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index e137a77d..ca22e453 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -183,6 +183,10 @@ impl From for TxCacheBundle { } } +impl CacheObject for TxCacheBundleResponse { + type Key = BundleKey; +} + impl TxCacheBundleResponse { /// Create a new bundle response from a bundle. pub const fn new(bundle: TxCacheBundle) -> Self { @@ -275,6 +279,10 @@ impl From for uuid::Uuid { } } +impl CacheObject for TxCacheSendBundleResponse { + type Key = BundleKey; +} + /// Response from the transaction cache `transactions` endpoint, containing a list of transactions. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TxCacheTransactionsResponse { @@ -341,6 +349,10 @@ impl From for B256 { } } +impl CacheObject for TxCacheSendTransactionResponse { + type Key = TxKey; +} + impl TxCacheSendTransactionResponse { /// Create a new transaction response from a transaction hash. pub const fn new(tx_hash: B256) -> Self { From cde92dc89f3898b518ebd3462fe2e63104b83d6e Mon Sep 17 00:00:00 2001 From: evalir Date: Tue, 11 Nov 2025 18:27:04 +0100 Subject: [PATCH 14/35] chore: inline, remove limit --- crates/tx-cache/src/client.rs | 6 +----- crates/tx-cache/src/types.rs | 17 ++--------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 47562413..56fae958 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -106,11 +106,7 @@ impl TxCache { .join(join) .inspect_err(|e| warn!(%e, "Failed to join URL. Not querying transaction cache."))?; - let request = if let Some(query) = query { - self.client.get(url).query(&query.cursor().to_query_object()) - } else { - self.client.get(url) - }; + let request = self.client.get(url).query(&query.map(|q| q.to_query_object())); // Get the result. request diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index ca22e453..fbd09a71 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -548,15 +548,12 @@ impl CursorKey for OrderKey { pub struct PaginationParams { /// The cursor to start from. cursor: C, - /// The number of items to return. - #[serde(skip_serializing_if = "Option::is_none")] - limit: Option, } impl PaginationParams { /// Creates a new instance of [`PaginationParams`]. - pub const fn new(cursor: C, limit: Option) -> Self { - Self { cursor, limit } + pub const fn new(cursor: C) -> Self { + Self { cursor } } /// Get the cursor to start from. @@ -568,16 +565,6 @@ impl PaginationParams { pub fn into_cursor(self) -> C { self.cursor } - - /// Set the limit for the pagination params. - pub fn with_limit(self, limit: u32) -> Self { - Self { limit: Some(limit), cursor: self.cursor } - } - - /// Get the limit for the items returned. - pub const fn limit(&self) -> Option { - self.limit - } } impl CursorKey for PaginationParams { From 003964ae8dcb266b12282ef5039d598ac28ec460 Mon Sep 17 00:00:00 2001 From: evalir Date: Tue, 11 Nov 2025 20:04:57 +0100 Subject: [PATCH 15/35] chore: review comments --- crates/tx-cache/Cargo.toml | 2 +- crates/tx-cache/src/types.rs | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/tx-cache/Cargo.toml b/crates/tx-cache/Cargo.toml index 2aafc1f9..2271e666 100644 --- a/crates/tx-cache/Cargo.toml +++ b/crates/tx-cache/Cargo.toml @@ -21,4 +21,4 @@ eyre.workspace = true reqwest.workspace = true serde = { workspace = true, features = ["derive"] } tracing.workspace = true -uuid = { workspace = true, features = ["serde", "v4"] } \ No newline at end of file +uuid = { workspace = true, features = ["serde"] } \ No newline at end of file diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index fbd09a71..d17879b6 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -450,19 +450,17 @@ impl TxCacheSendOrderResponse { pub struct PaginationInfo { /// The next cursor. next_cursor: Option, - /// Whether there is a next page. - has_next_page: bool, } impl PaginationInfo { /// Create a new [`PaginationInfo`]. - pub const fn new(next_cursor: Option, has_next_page: bool) -> Self { - Self { next_cursor, has_next_page } + pub const fn new(next_cursor: Option) -> Self { + Self { next_cursor } } /// Create an empty [`PaginationInfo`]. pub const fn empty() -> Self { - Self { next_cursor: None, has_next_page: false } + Self { next_cursor: None } } /// Get the next cursor. @@ -477,7 +475,7 @@ impl PaginationInfo { /// Check if there is a next page in the response. pub const fn has_next_page(&self) -> bool { - self.has_next_page + self.next_cursor.is_some() } } From 8c73ffc72e784c4b2f95d2e3d4cf385f92f3c17b Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 16:27:40 +0100 Subject: [PATCH 16/35] chore: make key optional on PaginationParams --- crates/tx-cache/src/client.rs | 5 ++++- crates/tx-cache/src/types.rs | 16 +++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 56fae958..3f9b5eb6 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -106,7 +106,10 @@ impl TxCache { .join(join) .inspect_err(|e| warn!(%e, "Failed to join URL. Not querying transaction cache."))?; - let request = self.client.get(url).query(&query.map(|q| q.to_query_object())); + let request = self + .client + .get(url) + .query(&query.and_then(|q| q.cursor().map(|c| c.to_query_object()))); // Get the result. request diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index d17879b6..6ea145ea 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -545,28 +545,22 @@ impl CursorKey for OrderKey { #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct PaginationParams { /// The cursor to start from. - cursor: C, + cursor: Option, } impl PaginationParams { /// Creates a new instance of [`PaginationParams`]. - pub const fn new(cursor: C) -> Self { + pub const fn new(cursor: Option) -> Self { Self { cursor } } /// Get the cursor to start from. - pub const fn cursor(&self) -> &C { - &self.cursor + pub const fn cursor(&self) -> Option<&C> { + self.cursor.as_ref() } /// Consumes the [`PaginationParams`] and returns the cursor. - pub fn into_cursor(self) -> C { + pub fn into_cursor(self) -> Option { self.cursor } } - -impl CursorKey for PaginationParams { - fn to_query_object(&self) -> HashMap { - self.cursor.to_query_object() - } -} From 39bb26ee7c44c128d9e604db205fe956421e79e9 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 18:47:40 +0100 Subject: [PATCH 17/35] chore: rework cursor types to manually deser for correct behavior --- crates/tx-cache/Cargo.toml | 6 +- crates/tx-cache/src/types.rs | 480 ++++++++++++++++++++++++++++++++++- 2 files changed, 478 insertions(+), 8 deletions(-) diff --git a/crates/tx-cache/Cargo.toml b/crates/tx-cache/Cargo.toml index 2271e666..403c8966 100644 --- a/crates/tx-cache/Cargo.toml +++ b/crates/tx-cache/Cargo.toml @@ -21,4 +21,8 @@ eyre.workspace = true reqwest.workspace = true serde = { workspace = true, features = ["derive"] } tracing.workspace = true -uuid = { workspace = true, features = ["serde"] } \ No newline at end of file +uuid = { workspace = true, features = ["serde"] } + +[dev-dependencies] +serde_urlencoded = "0.7.1" +uuid = { workspace = true, features = ["serde", "v4"] } \ No newline at end of file diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 6ea145ea..f860440b 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -1,13 +1,16 @@ //! The endpoints for the transaction cache. use alloy::{consensus::TxEnvelope, primitives::B256}; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{MapAccess, SeqAccess, Visitor}, + Deserialize, Deserializer, Serialize, +}; use signet_bundle::SignetEthBundle; use signet_types::SignedOrder; use std::collections::HashMap; use uuid::Uuid; /// A trait for allowing crusor keys to be converted into an URL query object. -pub trait CursorKey { +pub trait CursorKey: Serialize { /// Convert the cursor key into a URL query object. fn to_query_object(&self) -> HashMap; } @@ -480,7 +483,7 @@ impl PaginationInfo { } /// The query object keys for the transaction GET endpoint. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct TxKey { /// The transaction hash @@ -505,7 +508,7 @@ impl CursorKey for TxKey { } /// The query object keys for the bundle GET endpoint. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct BundleKey { /// The bundle id @@ -527,10 +530,10 @@ impl CursorKey for BundleKey { } /// The query object keys for the order GET endpoint. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct OrderKey { /// The order id - pub id: String, + pub id: B256, } impl CursorKey for OrderKey { @@ -542,9 +545,10 @@ impl CursorKey for OrderKey { } /// A query for pagination. -#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[derive(Clone, Debug, Serialize)] pub struct PaginationParams { /// The cursor to start from. + #[serde(flatten)] cursor: Option, } @@ -564,3 +568,465 @@ impl PaginationParams { self.cursor } } + +impl<'de> Deserialize<'de> for PaginationParams { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + enum Field { + TxnHash, + Score, + GlobalTransactionScoreKey, + } + + impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TxKeyVisitor; + + impl<'de> serde::de::Visitor<'de> for TxKeyVisitor { + type Value = Field; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + formatter.write_str("a TxKeyField") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match v { + "txnHash" => Ok(Field::TxnHash), + "score" => Ok(Field::Score), + "globalTransactionScoreKey" => Ok(Field::GlobalTransactionScoreKey), + _ => Err(serde::de::Error::unknown_field(v, FIELDS)), + } + } + } + + deserializer.deserialize_str(TxKeyVisitor) + } + } + + struct TxKeyVisitor; + + impl<'de> Visitor<'de> for TxKeyVisitor { + type Value = PaginationParams; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a PaginationParams") + } + + fn visit_seq(self, mut seq: S) -> Result, S::Error> + where + S: SeqAccess<'de>, + { + // We consider this a complete request if we have no txn hash. + let Some(txn_hash) = seq.next_element()? else { + // We consider this a complete request if we have no txn hash. + return Ok(PaginationParams::new(None)); + }; + + // For all other items, we require a score and a global transaction score key. + let score = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + let global_transaction_score_key = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; + Ok(PaginationParams::new(Some(TxKey { + txn_hash, + score, + global_transaction_score_key, + }))) + } + + fn visit_map(self, mut map: M) -> Result, M::Error> + where + M: MapAccess<'de>, + { + let mut txn_hash = None; + let mut score = None; + let mut global_transaction_score_key = None; + + while let Some(key) = map.next_key()? { + match key { + Field::TxnHash => { + if txn_hash.is_some() { + return Err(serde::de::Error::duplicate_field("txnHash")); + } + txn_hash = Some(map.next_value()?); + } + Field::Score => { + if score.is_some() { + return Err(serde::de::Error::duplicate_field("score")); + } + score = Some(map.next_value()?); + } + Field::GlobalTransactionScoreKey => { + if global_transaction_score_key.is_some() { + return Err(serde::de::Error::duplicate_field( + "globalTransactionScoreKey", + )); + } + global_transaction_score_key = Some(map.next_value()?); + } + } + } + + // We consider this a complete request if we have no txn hash. + let Some(txn_hash) = txn_hash else { + return Ok(PaginationParams::new(None)); + }; + + // For all other items, we require a score and a global transaction score key. + let score = score.ok_or_else(|| serde::de::Error::missing_field("score"))?; + let global_transaction_score_key = global_transaction_score_key + .ok_or_else(|| serde::de::Error::missing_field("globalTransactionScoreKey"))?; + + Ok(PaginationParams::new(Some(TxKey { + txn_hash, + score, + global_transaction_score_key, + }))) + } + } + + const FIELDS: &[&str] = &["txnHash", "score", "globalTransactionScoreKey"]; + deserializer.deserialize_struct("TxKey", FIELDS, TxKeyVisitor) + } +} + +impl<'de> Deserialize<'de> for PaginationParams { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + { + enum Field { + Id, + Score, + GlobalBundleScoreKey, + } + + impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BundleKeyVisitor; + + impl<'de> serde::de::Visitor<'de> for BundleKeyVisitor { + type Value = Field; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + formatter.write_str("a BundleKeyField") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match v { + "id" => Ok(Field::Id), + "score" => Ok(Field::Score), + "globalBundleScoreKey" => Ok(Field::GlobalBundleScoreKey), + _ => Err(serde::de::Error::unknown_field(v, FIELDS)), + } + } + } + + deserializer.deserialize_str(BundleKeyVisitor) + } + } + + struct BundleKeyVisitor; + + impl<'de> Visitor<'de> for BundleKeyVisitor { + type Value = PaginationParams; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a PaginationParams") + } + + fn visit_seq(self, mut seq: S) -> Result, S::Error> + where + S: SeqAccess<'de>, + { + // We consider this a complete request if we have no txn hash. + let Some(id) = seq.next_element()? else { + // We consider this a complete request if we have no txn hash. + return Ok(PaginationParams::new(None)); + }; + + // For all other items, we require a score and a global transaction score key. + let score = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + let global_bundle_score_key = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; + Ok(PaginationParams::new(Some(BundleKey { + id, + score, + global_bundle_score_key, + }))) + } + + fn visit_map(self, mut map: M) -> Result, M::Error> + where + M: MapAccess<'de>, + { + let mut id = None; + let mut score = None; + let mut global_bundle_score_key = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Id => { + if id.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id = Some(map.next_value()?); + } + Field::Score => { + if score.is_some() { + return Err(serde::de::Error::duplicate_field("score")); + } + score = Some(map.next_value()?); + } + Field::GlobalBundleScoreKey => { + if global_bundle_score_key.is_some() { + return Err(serde::de::Error::duplicate_field( + "globalBundleScoreKey", + )); + } + global_bundle_score_key = Some(map.next_value()?); + } + } + } + + // We consider this a complete request if we have no txn hash. + let Some(id) = id else { + return Ok(PaginationParams::new(None)); + }; + + // For all other items, we require a score and a global transaction score key. + let score = score.ok_or_else(|| serde::de::Error::missing_field("score"))?; + let global_bundle_score_key = global_bundle_score_key.ok_or_else(|| { + serde::de::Error::missing_field("globalTransactionScoreKey") + })?; + + Ok(PaginationParams::new(Some(BundleKey { + id, + score, + global_bundle_score_key, + }))) + } + } + + const FIELDS: &[&str] = &["id", "score", "globalBundleScoreKey"]; + deserializer.deserialize_struct("BundleKey", FIELDS, BundleKeyVisitor) + } + } +} + +impl<'de> Deserialize<'de> for PaginationParams { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + { + enum Field { + Id, + } + + impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct OrderKeyVisitor; + + impl<'de> serde::de::Visitor<'de> for OrderKeyVisitor { + type Value = Field; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + formatter.write_str("a OrderKeyField") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match v { + "id" => Ok(Field::Id), + _ => Err(serde::de::Error::unknown_field(v, FIELDS)), + } + } + } + + deserializer.deserialize_str(OrderKeyVisitor) + } + } + + struct OrderKeyVisitor; + + impl<'de> Visitor<'de> for OrderKeyVisitor { + type Value = PaginationParams; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a PaginationParams") + } + + fn visit_seq(self, mut seq: S) -> Result, S::Error> + where + S: SeqAccess<'de>, + { + let Some(id) = seq.next_element()? else { + return Ok(PaginationParams::new(None)); + }; + + Ok(PaginationParams::new(Some(OrderKey { id }))) + } + + fn visit_map(self, mut map: M) -> Result, M::Error> + where + M: MapAccess<'de>, + { + let mut id = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Id => { + if id.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id = Some(map.next_value()?); + } + } + } + + let Some(id) = id else { + return Ok(PaginationParams::new(None)); + }; + + Ok(PaginationParams::new(Some(OrderKey { id }))) + } + } + + const FIELDS: &[&str] = &["id"]; + deserializer.deserialize_struct("OrderKey", FIELDS, OrderKeyVisitor) + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[test] + fn test_pagination_params_simple_deser() { + let tx_key = TxKey { + txn_hash: B256::repeat_byte(0xaa), + score: 100, + global_transaction_score_key: "gtsk".to_string(), + }; + let params = PaginationParams::::new(Some(tx_key)); + let empty_params = PaginationParams::::new(None); + + let serialized = serde_urlencoded::to_string(¶ms).unwrap(); + let empty_serialized = serde_urlencoded::to_string(&empty_params).unwrap(); + assert_eq!(serialized, "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100&globalTransactionScoreKey=gtsk"); + assert_eq!(empty_serialized, ""); + } + + #[test] + fn test_pagination_params_partial_deser() { + let tx_key = TxKey { + txn_hash: B256::repeat_byte(0xaa), + score: 100, + global_transaction_score_key: "gtsk".to_string(), + }; + let params = PaginationParams::::new(Some(tx_key.clone())); + let serialized = serde_urlencoded::to_string(¶ms).unwrap(); + assert_eq!(serialized, "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100&globalTransactionScoreKey=gtsk"); + + let deserialized = + serde_urlencoded::from_str::>(&serialized).unwrap(); + assert_eq!(deserialized.cursor().unwrap(), &tx_key); + + let partial_query_string = + "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100"; + let partial_params = + serde_urlencoded::from_str::>(partial_query_string); + assert_eq!(partial_params.is_err(), true); + + let empty_query_string = ""; + let empty_params = + serde_urlencoded::from_str::>(empty_query_string); + assert_eq!(empty_params.is_ok(), true); + assert_eq!(empty_params.unwrap().cursor().is_none(), true); + } + + #[test] + fn test_pagination_params_bundle_deser() { + let bundle_key = BundleKey { + // This is our UUID. Nobody else use it. + id: Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(), + score: 100, + global_bundle_score_key: "gbsk".to_string(), + }; + + let params = PaginationParams::::new(Some(bundle_key.clone())); + let serialized = serde_urlencoded::to_string(¶ms).unwrap(); + assert_eq!( + serialized, + "id=5932d4bb-58d9-41a9-851d-8dd7f04ccc33&score=100&globalBundleScoreKey=gbsk" + ); + + let deserialized = + serde_urlencoded::from_str::>(&serialized).unwrap(); + assert_eq!(deserialized.cursor().unwrap(), &bundle_key); + + let partial_query_string = "id=5932d4bb-58d9-41a9-851d-8dd7f04ccc33&score=100"; + let partial_params = + serde_urlencoded::from_str::>(partial_query_string); + assert_eq!(partial_params.is_err(), true); + + let empty_query_string = ""; + let empty_params = + serde_urlencoded::from_str::>(empty_query_string); + assert_eq!(empty_params.is_err(), false); + } + + #[test] + fn test_pagination_params_order_deser() { + let order_key = OrderKey { id: B256::repeat_byte(0xaa) }; + + let params = PaginationParams::::new(Some(order_key.clone())); + let serialized = serde_urlencoded::to_string(¶ms).unwrap(); + assert_eq!( + serialized, + "id=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); + + let deserialized = + serde_urlencoded::from_str::>(&serialized).unwrap(); + assert_eq!(deserialized.cursor().unwrap(), &order_key); + } +} From 6ca953b00b5d1df17d1bd09e29d42ae8457f140e Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 18:47:58 +0100 Subject: [PATCH 18/35] chore: make client fns take key directly --- crates/tx-cache/src/client.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 3f9b5eb6..67195b49 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -93,11 +93,7 @@ impl TxCache { self.client.post(url).json(&obj).send().await?.error_for_status().map_err(Into::into) } - async fn get_inner( - &self, - join: &'static str, - query: Option>, - ) -> Result + async fn get_inner(&self, join: &'static str, query: Option) -> Result where T: DeserializeOwned + CacheObject, { @@ -106,10 +102,7 @@ impl TxCache { .join(join) .inspect_err(|e| warn!(%e, "Failed to join URL. Not querying transaction cache."))?; - let request = self - .client - .get(url) - .query(&query.and_then(|q| q.cursor().map(|c| c.to_query_object()))); + let request = self.client.get(url).query(&query); // Get the result. request @@ -149,7 +142,7 @@ impl TxCache { #[instrument(skip_all)] pub async fn get_transactions( &self, - query: Option>, + query: Option, ) -> Result, Error> { self.get_inner(TRANSACTIONS, query).await } @@ -158,7 +151,7 @@ impl TxCache { #[instrument(skip_all)] pub async fn get_orders( &self, - query: Option>, + query: Option, ) -> Result, Error> { self.get_inner(ORDERS, query).await } From 17670e86c599142fffd3b1abf412b4512166c083 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 18:53:08 +0100 Subject: [PATCH 19/35] clippy --- crates/tx-cache/src/client.rs | 4 ++-- crates/tx-cache/src/types.rs | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 67195b49..95d0838d 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -1,6 +1,6 @@ use crate::types::{ - CacheObject, CacheResponse, CursorKey, OrderKey, PaginationParams, TxCacheOrdersResponse, - TxCacheSendBundleResponse, TxCacheSendTransactionResponse, TxCacheTransactionsResponse, TxKey, + CacheObject, CacheResponse, OrderKey, TxCacheOrdersResponse, TxCacheSendBundleResponse, + TxCacheSendTransactionResponse, TxCacheTransactionsResponse, TxKey, }; use alloy::consensus::TxEnvelope; use eyre::Error; diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index f860440b..ff467798 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -974,13 +974,13 @@ mod tests { "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100"; let partial_params = serde_urlencoded::from_str::>(partial_query_string); - assert_eq!(partial_params.is_err(), true); + assert!(partial_params.is_err()); let empty_query_string = ""; let empty_params = serde_urlencoded::from_str::>(empty_query_string); - assert_eq!(empty_params.is_ok(), true); - assert_eq!(empty_params.unwrap().cursor().is_none(), true); + assert!(empty_params.is_ok()); + assert!(empty_params.unwrap().cursor().is_none()); } #[test] @@ -1006,19 +1006,20 @@ mod tests { let partial_query_string = "id=5932d4bb-58d9-41a9-851d-8dd7f04ccc33&score=100"; let partial_params = serde_urlencoded::from_str::>(partial_query_string); - assert_eq!(partial_params.is_err(), true); + assert!(partial_params.is_err()); let empty_query_string = ""; let empty_params = serde_urlencoded::from_str::>(empty_query_string); - assert_eq!(empty_params.is_err(), false); + assert!(empty_params.is_ok()); + assert!(empty_params.unwrap().cursor().is_none()); } #[test] fn test_pagination_params_order_deser() { let order_key = OrderKey { id: B256::repeat_byte(0xaa) }; - let params = PaginationParams::::new(Some(order_key.clone())); + let params = PaginationParams::::new(Some(order_key)); let serialized = serde_urlencoded::to_string(¶ms).unwrap(); assert_eq!( serialized, From 75126e8490eb636191ef0cda26917e2294859f94 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 19:00:08 +0100 Subject: [PATCH 20/35] chore: no more need for `CursorKey` --- crates/tx-cache/src/types.rs | 53 ++++-------------------------------- 1 file changed, 6 insertions(+), 47 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index ff467798..6c05501c 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -6,19 +6,12 @@ use serde::{ }; use signet_bundle::SignetEthBundle; use signet_types::SignedOrder; -use std::collections::HashMap; use uuid::Uuid; -/// A trait for allowing crusor keys to be converted into an URL query object. -pub trait CursorKey: Serialize { - /// Convert the cursor key into a URL query object. - fn to_query_object(&self) -> HashMap; -} - /// A trait for types that can be used as a cache object. pub trait CacheObject { /// The cursor key type for the cache object. - type Key: CursorKey; + type Key: Serialize + for<'a> Deserialize<'a>; } /// A response from the transaction cache, containing an item. @@ -117,10 +110,7 @@ where /// Consume the response and return the pagination info, if any. pub fn into_pagination_info(self) -> Option> { - match self { - Self::Paginated { pagination, .. } => Some(pagination), - Self::Unpaginated { .. } => None, - } + self.into_parts().1 } } @@ -450,12 +440,12 @@ impl TxCacheSendOrderResponse { /// Represents the pagination information from a transaction cache response. /// This applies to all GET endpoints that return a list of items. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PaginationInfo { +pub struct PaginationInfo { /// The next cursor. next_cursor: Option, } -impl PaginationInfo { +impl PaginationInfo { /// Create a new [`PaginationInfo`]. pub const fn new(next_cursor: Option) -> Self { Self { next_cursor } @@ -494,19 +484,6 @@ pub struct TxKey { pub global_transaction_score_key: String, } -impl CursorKey for TxKey { - fn to_query_object(&self) -> HashMap { - let mut map = HashMap::new(); - map.insert("txn_hash".to_string(), self.txn_hash.to_string()); - map.insert("score".to_string(), self.score.to_string()); - map.insert( - "global_transaction_score_key".to_string(), - self.global_transaction_score_key.to_string(), - ); - map - } -} - /// The query object keys for the bundle GET endpoint. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -519,16 +496,6 @@ pub struct BundleKey { pub global_bundle_score_key: String, } -impl CursorKey for BundleKey { - fn to_query_object(&self) -> HashMap { - let mut map = HashMap::new(); - map.insert("id".to_string(), self.id.to_string()); - map.insert("score".to_string(), self.score.to_string()); - map.insert("global_bundle_score_key".to_string(), self.global_bundle_score_key.to_string()); - map - } -} - /// The query object keys for the order GET endpoint. #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct OrderKey { @@ -536,23 +503,15 @@ pub struct OrderKey { pub id: B256, } -impl CursorKey for OrderKey { - fn to_query_object(&self) -> HashMap { - let mut map = HashMap::new(); - map.insert("id".to_string(), self.id.to_string()); - map - } -} - /// A query for pagination. #[derive(Clone, Debug, Serialize)] -pub struct PaginationParams { +pub struct PaginationParams Deserialize<'a>> { /// The cursor to start from. #[serde(flatten)] cursor: Option, } -impl PaginationParams { +impl Deserialize<'a>> PaginationParams { /// Creates a new instance of [`PaginationParams`]. pub const fn new(cursor: Option) -> Self { Self { cursor } From a4f491dff2006d248800be5fa0f105791b5aa0b8 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 19:01:08 +0100 Subject: [PATCH 21/35] chore: simplify bounds further --- crates/tx-cache/src/types.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 6c05501c..5172cecd 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -17,10 +17,7 @@ pub trait CacheObject { /// A response from the transaction cache, containing an item. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] -pub enum CacheResponse -where - T::Key: Serialize + for<'a> Deserialize<'a>, -{ +pub enum CacheResponse { /// A paginated response, containing the inner item and a pagination info. Paginated { /// The actual item. @@ -37,17 +34,11 @@ where }, } -impl CacheObject for CacheResponse -where - T::Key: Serialize + for<'a> Deserialize<'a>, -{ +impl CacheObject for CacheResponse { type Key = T::Key; } -impl CacheResponse -where - T::Key: Serialize + for<'a> Deserialize<'a>, -{ +impl CacheResponse { /// Create a new paginated response from a list of items and a pagination info. pub const fn paginated(inner: T, pagination: PaginationInfo) -> Self { Self::Paginated { inner, pagination } From 8fa47ec58ff8a097046149699f430ee6098ea3b5 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 19:12:09 +0100 Subject: [PATCH 22/35] chore: correct visitor impl --- crates/tx-cache/src/types.rs | 38 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 5172cecd..ae18b018 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -577,7 +577,7 @@ impl<'de> Deserialize<'de> for PaginationParams { where S: SeqAccess<'de>, { - // We consider this a complete request if we have no txn hash. + // We consider this a complete request if we have no elements in the sequence. let Some(txn_hash) = seq.next_element()? else { // We consider this a complete request if we have no txn hash. return Ok(PaginationParams::new(None)); @@ -630,9 +630,19 @@ impl<'de> Deserialize<'de> for PaginationParams { } } - // We consider this a complete request if we have no txn hash. - let Some(txn_hash) = txn_hash else { - return Ok(PaginationParams::new(None)); + // We consider this a complete request if we have no txn hash and no other fields are present. + let txn_hash = match txn_hash { + Some(hash) => hash, + None => { + if score.is_some() || global_transaction_score_key.is_some() { + return Err(serde::de::Error::invalid_length( + score.is_some() as usize + + global_transaction_score_key.is_some() as usize, + &self, + )); + } + return Ok(PaginationParams::new(None)); + } }; // For all other items, we require a score and a global transaction score key. @@ -712,9 +722,8 @@ impl<'de> Deserialize<'de> for PaginationParams { where S: SeqAccess<'de>, { - // We consider this a complete request if we have no txn hash. + // We consider this a complete request if we have no elements in the sequence. let Some(id) = seq.next_element()? else { - // We consider this a complete request if we have no txn hash. return Ok(PaginationParams::new(None)); }; @@ -765,17 +774,22 @@ impl<'de> Deserialize<'de> for PaginationParams { } } - // We consider this a complete request if we have no txn hash. + // We consider this a complete request if we have no id and no other fields are present. let Some(id) = id else { + if score.is_some() || global_bundle_score_key.is_some() { + return Err(serde::de::Error::invalid_length( + score.is_some() as usize + + global_bundle_score_key.is_some() as usize, + &self, + )); + } return Ok(PaginationParams::new(None)); }; - // For all other items, we require a score and a global transaction score key. + // For all other items, we require a score and a global bundle score key. let score = score.ok_or_else(|| serde::de::Error::missing_field("score"))?; - let global_bundle_score_key = global_bundle_score_key.ok_or_else(|| { - serde::de::Error::missing_field("globalTransactionScoreKey") - })?; - + let global_bundle_score_key = global_bundle_score_key + .ok_or_else(|| serde::de::Error::missing_field("globalBundleScoreKey"))?; Ok(PaginationParams::new(Some(BundleKey { id, score, From 2d22d48890f3259c37e8f15c28d3700e9edeae43 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 19:13:33 +0100 Subject: [PATCH 23/35] chore: test edge cases properly --- crates/tx-cache/src/types.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index ae18b018..a7d681c9 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -934,6 +934,11 @@ mod tests { serde_urlencoded::from_str::>(&serialized).unwrap(); assert_eq!(deserialized.cursor().unwrap(), &tx_key); + let partial_query_string = "score=100&globalTransactionScoreKey=gtsk"; + let partial_params = + serde_urlencoded::from_str::>(partial_query_string); + assert!(partial_params.is_err()); + let partial_query_string = "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100"; let partial_params = @@ -967,6 +972,11 @@ mod tests { serde_urlencoded::from_str::>(&serialized).unwrap(); assert_eq!(deserialized.cursor().unwrap(), &bundle_key); + let partial_query_string = "score=100&globalBundleScoreKey=gbsk"; + let partial_params = + serde_urlencoded::from_str::>(partial_query_string); + assert!(partial_params.is_err()); + let partial_query_string = "id=5932d4bb-58d9-41a9-851d-8dd7f04ccc33&score=100"; let partial_params = serde_urlencoded::from_str::>(partial_query_string); From 2be93259941dac2b441e5d08dc255f602dad1cfb Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 20:03:56 +0100 Subject: [PATCH 24/35] chore: rm `PaginationInfo` --- crates/tx-cache/src/types.rs | 68 ++++++++++-------------------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index a7d681c9..3eff9eab 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -1,7 +1,7 @@ //! The endpoints for the transaction cache. use alloy::{consensus::TxEnvelope, primitives::B256}; use serde::{ - de::{MapAccess, SeqAccess, Visitor}, + de::{DeserializeOwned, MapAccess, SeqAccess, Visitor}, Deserialize, Deserializer, Serialize, }; use signet_bundle::SignetEthBundle; @@ -11,20 +11,20 @@ use uuid::Uuid; /// A trait for types that can be used as a cache object. pub trait CacheObject { /// The cursor key type for the cache object. - type Key: Serialize + for<'a> Deserialize<'a>; + type Key: Serialize + DeserializeOwned; } /// A response from the transaction cache, containing an item. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum CacheResponse { - /// A paginated response, containing the inner item and a pagination info. + /// A paginated response, containing the inner item and a next cursor. Paginated { /// The actual item. #[serde(flatten)] inner: T, - /// The pagination info. - pagination: PaginationInfo, + /// The next cursor for pagination, if any. + next_cursor: Option, }, /// An unpaginated response, containing the actual item. Unpaginated { @@ -40,8 +40,8 @@ impl CacheObject for CacheResponse { impl CacheResponse { /// Create a new paginated response from a list of items and a pagination info. - pub const fn paginated(inner: T, pagination: PaginationInfo) -> Self { - Self::Paginated { inner, pagination } + pub const fn paginated(inner: T, pagination: T::Key) -> Self { + Self::Paginated { inner, next_cursor: Some(pagination) } } /// Create a new unpaginated response from a list of items. @@ -65,14 +65,19 @@ impl CacheResponse { } } - /// Return the pagination info, if any. - pub const fn pagination_info(&self) -> Option<&PaginationInfo> { + /// Return the next cursor for pagination, if any. + pub const fn next_cursor(&self) -> Option<&T::Key> { match self { - Self::Paginated { pagination, .. } => Some(pagination), + Self::Paginated { next_cursor, .. } => next_cursor.as_ref(), Self::Unpaginated { .. } => None, } } + /// Check if the response has more items to fetch. + pub const fn has_more(&self) -> bool { + self.next_cursor().is_some() + } + /// Check if the response is paginated. pub const fn is_paginated(&self) -> bool { matches!(self, Self::Paginated { .. }) @@ -92,15 +97,15 @@ impl CacheResponse { } /// Consume the response and return the parts. - pub fn into_parts(self) -> (T, Option>) { + pub fn into_parts(self) -> (T, Option) { match self { - Self::Paginated { inner, pagination } => (inner, Some(pagination)), + Self::Paginated { inner, next_cursor } => (inner, next_cursor), Self::Unpaginated { inner } => (inner, None), } } - /// Consume the response and return the pagination info, if any. - pub fn into_pagination_info(self) -> Option> { + /// Consume the response and return the next cursor for pagination, if any. + pub fn into_next_cursor(self) -> Option { self.into_parts().1 } } @@ -428,41 +433,6 @@ impl TxCacheSendOrderResponse { } } -/// Represents the pagination information from a transaction cache response. -/// This applies to all GET endpoints that return a list of items. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PaginationInfo { - /// The next cursor. - next_cursor: Option, -} - -impl PaginationInfo { - /// Create a new [`PaginationInfo`]. - pub const fn new(next_cursor: Option) -> Self { - Self { next_cursor } - } - - /// Create an empty [`PaginationInfo`]. - pub const fn empty() -> Self { - Self { next_cursor: None } - } - - /// Get the next cursor. - pub const fn next_cursor(&self) -> Option<&C> { - self.next_cursor.as_ref() - } - - /// Consume the [`PaginationInfo`] and return the next cursor. - pub fn into_next_cursor(self) -> Option { - self.next_cursor - } - - /// Check if there is a next page in the response. - pub const fn has_next_page(&self) -> bool { - self.next_cursor.is_some() - } -} - /// The query object keys for the transaction GET endpoint. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] From a93669d8e9f339be99c9aa883f2d65221d9d45ac Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 20:23:23 +0100 Subject: [PATCH 25/35] chore: modify tests to serialize without `PaginationParams` --- crates/tx-cache/src/types.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 3eff9eab..592ba897 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -880,8 +880,8 @@ mod tests { score: 100, global_transaction_score_key: "gtsk".to_string(), }; - let params = PaginationParams::::new(Some(tx_key)); - let empty_params = PaginationParams::::new(None); + let params = tx_key.clone(); + let empty_params: Option = None; let serialized = serde_urlencoded::to_string(¶ms).unwrap(); let empty_serialized = serde_urlencoded::to_string(&empty_params).unwrap(); @@ -896,8 +896,7 @@ mod tests { score: 100, global_transaction_score_key: "gtsk".to_string(), }; - let params = PaginationParams::::new(Some(tx_key.clone())); - let serialized = serde_urlencoded::to_string(¶ms).unwrap(); + let serialized = serde_urlencoded::to_string(&tx_key).unwrap(); assert_eq!(serialized, "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100&globalTransactionScoreKey=gtsk"); let deserialized = @@ -931,8 +930,7 @@ mod tests { global_bundle_score_key: "gbsk".to_string(), }; - let params = PaginationParams::::new(Some(bundle_key.clone())); - let serialized = serde_urlencoded::to_string(¶ms).unwrap(); + let serialized = serde_urlencoded::to_string(&bundle_key).unwrap(); assert_eq!( serialized, "id=5932d4bb-58d9-41a9-851d-8dd7f04ccc33&score=100&globalBundleScoreKey=gbsk" @@ -962,9 +960,7 @@ mod tests { #[test] fn test_pagination_params_order_deser() { let order_key = OrderKey { id: B256::repeat_byte(0xaa) }; - - let params = PaginationParams::::new(Some(order_key)); - let serialized = serde_urlencoded::to_string(¶ms).unwrap(); + let serialized = serde_urlencoded::to_string(&order_key).unwrap(); assert_eq!( serialized, "id=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" From 3a61bb6f3b12d761e3cdc485595b406601ce14d8 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 20:27:34 +0100 Subject: [PATCH 26/35] chore: clippy --- crates/tx-cache/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 592ba897..67547308 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -960,7 +960,7 @@ mod tests { #[test] fn test_pagination_params_order_deser() { let order_key = OrderKey { id: B256::repeat_byte(0xaa) }; - let serialized = serde_urlencoded::to_string(&order_key).unwrap(); + let serialized = serde_urlencoded::to_string(order_key).unwrap(); assert_eq!( serialized, "id=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" From c9ca389a288f491c686ca81c7b562cfafa6f0ee9 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 20:28:12 +0100 Subject: [PATCH 27/35] chore: simplify client get --- crates/tx-cache/src/client.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 95d0838d..0bae9568 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -102,10 +102,9 @@ impl TxCache { .join(join) .inspect_err(|e| warn!(%e, "Failed to join URL. Not querying transaction cache."))?; - let request = self.client.get(url).query(&query); - - // Get the result. - request + self.client + .get(url) + .query(&query) .send() .await .inspect_err(|e| warn!(%e, "Failed to get object from transaction cache."))? From ad594e582e0c6c0862cffe379801381aab693a16 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 20:46:57 +0100 Subject: [PATCH 28/35] chore: rename `PaginationParams to `CursorPayload` --- crates/tx-cache/src/types.rs | 84 ++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 67547308..4e313cde 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -464,15 +464,15 @@ pub struct OrderKey { pub id: B256, } -/// A query for pagination. +/// A deserialization helper for cursors keys. #[derive(Clone, Debug, Serialize)] -pub struct PaginationParams Deserialize<'a>> { - /// The cursor to start from. +pub struct CursorPayload Deserialize<'a>> { + // The cursor key. #[serde(flatten)] cursor: Option, } -impl Deserialize<'a>> PaginationParams { +impl Deserialize<'a>> CursorPayload { /// Creates a new instance of [`PaginationParams`]. pub const fn new(cursor: Option) -> Self { Self { cursor } @@ -489,7 +489,7 @@ impl Deserialize<'a>> PaginationParams { } } -impl<'de> Deserialize<'de> for PaginationParams { +impl<'de> Deserialize<'de> for CursorPayload { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -537,20 +537,20 @@ impl<'de> Deserialize<'de> for PaginationParams { struct TxKeyVisitor; impl<'de> Visitor<'de> for TxKeyVisitor { - type Value = PaginationParams; + type Value = CursorPayload; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("a PaginationParams") } - fn visit_seq(self, mut seq: S) -> Result, S::Error> + fn visit_seq(self, mut seq: S) -> Result, S::Error> where S: SeqAccess<'de>, { // We consider this a complete request if we have no elements in the sequence. let Some(txn_hash) = seq.next_element()? else { // We consider this a complete request if we have no txn hash. - return Ok(PaginationParams::new(None)); + return Ok(CursorPayload::new(None)); }; // For all other items, we require a score and a global transaction score key. @@ -560,14 +560,14 @@ impl<'de> Deserialize<'de> for PaginationParams { let global_transaction_score_key = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; - Ok(PaginationParams::new(Some(TxKey { + Ok(CursorPayload::new(Some(TxKey { txn_hash, score, global_transaction_score_key, }))) } - fn visit_map(self, mut map: M) -> Result, M::Error> + fn visit_map(self, mut map: M) -> Result, M::Error> where M: MapAccess<'de>, { @@ -611,7 +611,7 @@ impl<'de> Deserialize<'de> for PaginationParams { &self, )); } - return Ok(PaginationParams::new(None)); + return Ok(CursorPayload::new(None)); } }; @@ -620,7 +620,7 @@ impl<'de> Deserialize<'de> for PaginationParams { let global_transaction_score_key = global_transaction_score_key .ok_or_else(|| serde::de::Error::missing_field("globalTransactionScoreKey"))?; - Ok(PaginationParams::new(Some(TxKey { + Ok(CursorPayload::new(Some(TxKey { txn_hash, score, global_transaction_score_key, @@ -633,7 +633,7 @@ impl<'de> Deserialize<'de> for PaginationParams { } } -impl<'de> Deserialize<'de> for PaginationParams { +impl<'de> Deserialize<'de> for CursorPayload { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -682,19 +682,19 @@ impl<'de> Deserialize<'de> for PaginationParams { struct BundleKeyVisitor; impl<'de> Visitor<'de> for BundleKeyVisitor { - type Value = PaginationParams; + type Value = CursorPayload; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("a PaginationParams") } - fn visit_seq(self, mut seq: S) -> Result, S::Error> + fn visit_seq(self, mut seq: S) -> Result, S::Error> where S: SeqAccess<'de>, { // We consider this a complete request if we have no elements in the sequence. let Some(id) = seq.next_element()? else { - return Ok(PaginationParams::new(None)); + return Ok(CursorPayload::new(None)); }; // For all other items, we require a score and a global transaction score key. @@ -704,14 +704,10 @@ impl<'de> Deserialize<'de> for PaginationParams { let global_bundle_score_key = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; - Ok(PaginationParams::new(Some(BundleKey { - id, - score, - global_bundle_score_key, - }))) + Ok(CursorPayload::new(Some(BundleKey { id, score, global_bundle_score_key }))) } - fn visit_map(self, mut map: M) -> Result, M::Error> + fn visit_map(self, mut map: M) -> Result, M::Error> where M: MapAccess<'de>, { @@ -753,18 +749,14 @@ impl<'de> Deserialize<'de> for PaginationParams { &self, )); } - return Ok(PaginationParams::new(None)); + return Ok(CursorPayload::new(None)); }; // For all other items, we require a score and a global bundle score key. let score = score.ok_or_else(|| serde::de::Error::missing_field("score"))?; let global_bundle_score_key = global_bundle_score_key .ok_or_else(|| serde::de::Error::missing_field("globalBundleScoreKey"))?; - Ok(PaginationParams::new(Some(BundleKey { - id, - score, - global_bundle_score_key, - }))) + Ok(CursorPayload::new(Some(BundleKey { id, score, global_bundle_score_key }))) } } @@ -774,7 +766,7 @@ impl<'de> Deserialize<'de> for PaginationParams { } } -impl<'de> Deserialize<'de> for PaginationParams { +impl<'de> Deserialize<'de> for CursorPayload { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -819,24 +811,24 @@ impl<'de> Deserialize<'de> for PaginationParams { struct OrderKeyVisitor; impl<'de> Visitor<'de> for OrderKeyVisitor { - type Value = PaginationParams; + type Value = CursorPayload; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("a PaginationParams") } - fn visit_seq(self, mut seq: S) -> Result, S::Error> + fn visit_seq(self, mut seq: S) -> Result, S::Error> where S: SeqAccess<'de>, { let Some(id) = seq.next_element()? else { - return Ok(PaginationParams::new(None)); + return Ok(CursorPayload::new(None)); }; - Ok(PaginationParams::new(Some(OrderKey { id }))) + Ok(CursorPayload::new(Some(OrderKey { id }))) } - fn visit_map(self, mut map: M) -> Result, M::Error> + fn visit_map(self, mut map: M) -> Result, M::Error> where M: MapAccess<'de>, { @@ -854,10 +846,10 @@ impl<'de> Deserialize<'de> for PaginationParams { } let Some(id) = id else { - return Ok(PaginationParams::new(None)); + return Ok(CursorPayload::new(None)); }; - Ok(PaginationParams::new(Some(OrderKey { id }))) + Ok(CursorPayload::new(Some(OrderKey { id }))) } } @@ -899,24 +891,22 @@ mod tests { let serialized = serde_urlencoded::to_string(&tx_key).unwrap(); assert_eq!(serialized, "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100&globalTransactionScoreKey=gtsk"); - let deserialized = - serde_urlencoded::from_str::>(&serialized).unwrap(); + let deserialized = serde_urlencoded::from_str::>(&serialized).unwrap(); assert_eq!(deserialized.cursor().unwrap(), &tx_key); let partial_query_string = "score=100&globalTransactionScoreKey=gtsk"; let partial_params = - serde_urlencoded::from_str::>(partial_query_string); + serde_urlencoded::from_str::>(partial_query_string); assert!(partial_params.is_err()); let partial_query_string = "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100"; let partial_params = - serde_urlencoded::from_str::>(partial_query_string); + serde_urlencoded::from_str::>(partial_query_string); assert!(partial_params.is_err()); let empty_query_string = ""; - let empty_params = - serde_urlencoded::from_str::>(empty_query_string); + let empty_params = serde_urlencoded::from_str::>(empty_query_string); assert!(empty_params.is_ok()); assert!(empty_params.unwrap().cursor().is_none()); } @@ -937,22 +927,22 @@ mod tests { ); let deserialized = - serde_urlencoded::from_str::>(&serialized).unwrap(); + serde_urlencoded::from_str::>(&serialized).unwrap(); assert_eq!(deserialized.cursor().unwrap(), &bundle_key); let partial_query_string = "score=100&globalBundleScoreKey=gbsk"; let partial_params = - serde_urlencoded::from_str::>(partial_query_string); + serde_urlencoded::from_str::>(partial_query_string); assert!(partial_params.is_err()); let partial_query_string = "id=5932d4bb-58d9-41a9-851d-8dd7f04ccc33&score=100"; let partial_params = - serde_urlencoded::from_str::>(partial_query_string); + serde_urlencoded::from_str::>(partial_query_string); assert!(partial_params.is_err()); let empty_query_string = ""; let empty_params = - serde_urlencoded::from_str::>(empty_query_string); + serde_urlencoded::from_str::>(empty_query_string); assert!(empty_params.is_ok()); assert!(empty_params.unwrap().cursor().is_none()); } @@ -967,7 +957,7 @@ mod tests { ); let deserialized = - serde_urlencoded::from_str::>(&serialized).unwrap(); + serde_urlencoded::from_str::>(&serialized).unwrap(); assert_eq!(deserialized.cursor().unwrap(), &order_key); } } From 252ada07d05aae6f5e57bc6eb0098f512692068c Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 21:54:01 +0100 Subject: [PATCH 29/35] chore: more tests --- crates/tx-cache/Cargo.toml | 3 +- crates/tx-cache/src/types.rs | 156 +++++++++++++++++++++++++++++++---- 2 files changed, 143 insertions(+), 16 deletions(-) diff --git a/crates/tx-cache/Cargo.toml b/crates/tx-cache/Cargo.toml index 403c8966..3be91014 100644 --- a/crates/tx-cache/Cargo.toml +++ b/crates/tx-cache/Cargo.toml @@ -25,4 +25,5 @@ uuid = { workspace = true, features = ["serde"] } [dev-dependencies] serde_urlencoded = "0.7.1" -uuid = { workspace = true, features = ["serde", "v4"] } \ No newline at end of file +uuid = { workspace = true, features = ["serde", "v4"] } +serde_json.workspace = true \ No newline at end of file diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 4e313cde..75e04441 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -15,8 +15,8 @@ pub trait CacheObject { } /// A response from the transaction cache, containing an item. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged, rename_all_fields = "camelCase")] pub enum CacheResponse { /// A paginated response, containing the inner item and a next cursor. Paginated { @@ -24,7 +24,7 @@ pub enum CacheResponse { #[serde(flatten)] inner: T, /// The next cursor for pagination, if any. - next_cursor: Option, + next_cursor: T::Key, }, /// An unpaginated response, containing the actual item. Unpaginated { @@ -41,7 +41,7 @@ impl CacheObject for CacheResponse { impl CacheResponse { /// Create a new paginated response from a list of items and a pagination info. pub const fn paginated(inner: T, pagination: T::Key) -> Self { - Self::Paginated { inner, next_cursor: Some(pagination) } + Self::Paginated { inner, next_cursor: pagination } } /// Create a new unpaginated response from a list of items. @@ -68,7 +68,7 @@ impl CacheResponse { /// Return the next cursor for pagination, if any. pub const fn next_cursor(&self) -> Option<&T::Key> { match self { - Self::Paginated { next_cursor, .. } => next_cursor.as_ref(), + Self::Paginated { next_cursor, .. } => Some(next_cursor), Self::Unpaginated { .. } => None, } } @@ -99,7 +99,7 @@ impl CacheResponse { /// Consume the response and return the parts. pub fn into_parts(self) -> (T, Option) { match self { - Self::Paginated { inner, next_cursor } => (inner, next_cursor), + Self::Paginated { inner, next_cursor } => (inner, Some(next_cursor)), Self::Unpaginated { inner } => (inner, None), } } @@ -112,7 +112,7 @@ impl CacheResponse { /// A bundle response from the transaction cache, containing a UUID and a /// [`SignetEthBundle`]. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TxCacheBundle { /// The bundle id (a UUID) pub id: uuid::Uuid, @@ -154,7 +154,7 @@ impl TxCacheBundle { } /// A response from the transaction cache, containing a single bundle. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TxCacheBundleResponse { /// The bundle. pub bundle: TxCacheBundle, @@ -196,7 +196,7 @@ impl TxCacheBundleResponse { } /// Response from the transaction cache `bundles` endpoint, containing a list of bundles. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TxCacheBundlesResponse { /// the list of bundles pub bundles: Vec, @@ -243,7 +243,7 @@ impl TxCacheBundlesResponse { } /// Represents a response to successfully adding or updating a bundle in the transaction cache. -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TxCacheSendBundleResponse { /// The bundle id (a UUID) pub id: uuid::Uuid, @@ -273,7 +273,7 @@ impl CacheObject for TxCacheSendBundleResponse { } /// Response from the transaction cache `transactions` endpoint, containing a list of transactions. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TxCacheTransactionsResponse { /// The list of transactions. pub transactions: Vec, @@ -320,7 +320,7 @@ impl TxCacheTransactionsResponse { } /// Response from the transaction cache to successfully adding a transaction. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct TxCacheSendTransactionResponse { /// The transaction hash pub tx_hash: B256, @@ -362,7 +362,7 @@ impl TxCacheSendTransactionResponse { } /// Response from the transaction cache `orders` endpoint, containing a list of signed orders. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TxCacheOrdersResponse { /// The list of signed orders. pub orders: Vec, @@ -404,7 +404,7 @@ impl TxCacheOrdersResponse { } /// Response from the transaction cache to successfully adding an order. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct TxCacheSendOrderResponse { /// The order id pub id: B256, @@ -861,9 +861,135 @@ impl<'de> Deserialize<'de> for CursorPayload { #[cfg(test)] mod tests { + use super::*; use std::str::FromStr; - use super::*; + fn dummy_bundle_with_id(id: Uuid) -> TxCacheBundle { + TxCacheBundle { + id, + bundle: SignetEthBundle { + bundle: alloy::rpc::types::mev::EthSendBundle { + txs: vec![], + block_number: 0, + min_timestamp: None, + max_timestamp: None, + reverting_tx_hashes: vec![], + replacement_uuid: Some(id.to_string()), + dropping_tx_hashes: vec![], + refund_percent: None, + refund_recipient: None, + refund_tx_hashes: vec![], + extra_fields: Default::default(), + }, + host_fills: None, + host_txs: vec![], + }, + } + } + + #[test] + fn test_unpaginated_cache_response_deser() { + let cache_response = CacheResponse::Unpaginated { + inner: TxCacheTransactionsResponse { transactions: vec![] }, + }; + let expected_json = r#"{"transactions":[]}"#; + let serialized = serde_json::to_string(&cache_response).unwrap(); + assert_eq!(serialized, expected_json); + let deserialized = + serde_json::from_str::>(&serialized) + .unwrap(); + assert_eq!(deserialized, cache_response); + } + + #[test] + fn test_paginated_cache_response_deser() { + let cache_response = CacheResponse::Paginated { + inner: TxCacheTransactionsResponse { transactions: vec![] }, + next_cursor: TxKey { + txn_hash: B256::repeat_byte(0xaa), + score: 100, + global_transaction_score_key: "gtsk".to_string(), + }, + }; + let expected_json = r#"{"transactions":[],"nextCursor":{"txnHash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","score":100,"globalTransactionScoreKey":"gtsk"}}"#; + let serialized = serde_json::to_string(&cache_response).unwrap(); + assert_eq!(serialized, expected_json); + let deserialized = + serde_json::from_str::>(&expected_json) + .unwrap(); + assert_eq!(deserialized, cache_response); + } + + // `serde_json` should be able to deserialize the old format, regardless if there's pagination information on the response. + // This mimics the behavior of the types pre-pagination. + #[test] + fn test_backwards_compatibility_cache_response_deser() { + let expected_json = r#"{"transactions":[],"nextCursor":{"txnHash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","score":100,"globalTransactionScoreKey":"gtsk"}}"#; + let deserialized = + serde_json::from_str::(&expected_json).unwrap(); + assert_eq!(deserialized, TxCacheTransactionsResponse { transactions: vec![] }); + } + + // `serde_json` should be able to deserialize the old format, regardless if there's pagination information on the response. + // This mimics the behavior of the types pre-pagination. + #[test] + fn test_backwards_compatibility_cache_bundle_response_deser() { + let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}]}"#; + let uuid = Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(); + + let deserialized = serde_json::from_str::(&expected_json).unwrap(); + + assert_eq!( + deserialized, + TxCacheBundlesResponse { bundles: vec![dummy_bundle_with_id(uuid)] } + ); + } + + // `serde_json` should be able to deserialize the old format, regardless if there's pagination information on the response. + // This mimics the behavior of the types pre-pagination. + #[test] + fn test_backwards_compatibility_cache_order_response_deser() { + let expected_json = r#"{"orders":[{"permit":{"permitted":[{"token":"0x0b8bc5e60ee10957e0d1a0d95598fa63e65605e2","amount":"0xf4240"}],"nonce":"0x637253c1eb651","deadline":"0x6846fde6"},"owner":"0x492e9c316f073fe4de9d665221568cdad1a7e95b","signature":"0x73e31a7c80f02840c4e0671230c408a5cbc7cddefc780db4dd102eed8e87c5740fc89944eb8e5756edd368ed755415ed090b043d1740ee6869c20cb1676329621c","outputs":[{"token":"0x885f8db528dc8a38aa3ddad9d3f619746b4a6a81","amount":"0xf4240","recipient":"0x492e9c316f073fe4de9d665221568cdad1a7e95b","chainId":3151908}]}], "id":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}"#; + let _ = serde_json::from_str::(&expected_json).unwrap(); + } + + #[test] + fn test_unpaginated_cache_bundle_response_deser() { + let cache_response = CacheResponse::Unpaginated { + inner: TxCacheBundlesResponse { + bundles: vec![dummy_bundle_with_id( + Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(), + )], + }, + }; + let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}]}"#; + let serialized = serde_json::to_string(&cache_response).unwrap(); + assert_eq!(serialized, expected_json); + let deserialized = + serde_json::from_str::>(&expected_json).unwrap(); + assert_eq!(deserialized, cache_response); + } + + #[test] + fn test_paginated_cache_bundle_response_deser() { + let uuid = Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(); + + let cache_response = CacheResponse::Paginated { + inner: TxCacheBundlesResponse { bundles: vec![dummy_bundle_with_id(uuid)] }, + next_cursor: BundleKey { + id: uuid, + score: 100, + global_bundle_score_key: "gbsk".to_string(), + }, + }; + let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}],"nextCursor":{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","score":100,"globalBundleScoreKey":"gbsk"}}"#; + let serialized = serde_json::to_string(&cache_response).unwrap(); + dbg!(&serialized); + assert_eq!(serialized, expected_json); + let deserialized = + serde_json::from_str::>(&expected_json).unwrap(); + assert_eq!(deserialized, cache_response); + } #[test] fn test_pagination_params_simple_deser() { From 4e3fbde13ff20e58637a89bee13e0590c39152d5 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 22:02:02 +0100 Subject: [PATCH 30/35] feat: turn cacheresponse enum into struct --- crates/tx-cache/src/types.rs | 84 ++++++++++++++---------------------- 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 75e04441..5c7867da 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -16,22 +16,14 @@ pub trait CacheObject { /// A response from the transaction cache, containing an item. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(untagged, rename_all_fields = "camelCase")] -pub enum CacheResponse { - /// A paginated response, containing the inner item and a next cursor. - Paginated { - /// The actual item. - #[serde(flatten)] - inner: T, - /// The next cursor for pagination, if any. - next_cursor: T::Key, - }, - /// An unpaginated response, containing the actual item. - Unpaginated { - /// The actual item. - #[serde(flatten)] - inner: T, - }, +#[serde(rename_all = "camelCase")] +pub struct CacheResponse { + /// The response. + #[serde(flatten)] + inner: T, + /// The next cursor for pagination, if any. + #[serde(default, skip_serializing_if = "Option::is_none")] + next_cursor: Option, } impl CacheObject for CacheResponse { @@ -41,35 +33,32 @@ impl CacheObject for CacheResponse { impl CacheResponse { /// Create a new paginated response from a list of items and a pagination info. pub const fn paginated(inner: T, pagination: T::Key) -> Self { - Self::Paginated { inner, next_cursor: pagination } + Self { inner, next_cursor: Some(pagination) } } /// Create a new unpaginated response from a list of items. pub const fn unpaginated(inner: T) -> Self { - Self::Unpaginated { inner } + Self { inner, next_cursor: None } } /// Return a reference to the inner value. pub const fn inner(&self) -> &T { match self { - Self::Paginated { inner, .. } => inner, - Self::Unpaginated { inner } => inner, + Self { inner, .. } => inner, } } /// Return a mutable reference to the inner value. pub const fn inner_mut(&mut self) -> &mut T { match self { - Self::Paginated { inner, .. } => inner, - Self::Unpaginated { inner } => inner, + Self { inner, .. } => inner, } } /// Return the next cursor for pagination, if any. pub const fn next_cursor(&self) -> Option<&T::Key> { match self { - Self::Paginated { next_cursor, .. } => Some(next_cursor), - Self::Unpaginated { .. } => None, + Self { next_cursor, .. } => next_cursor.as_ref(), } } @@ -80,27 +69,25 @@ impl CacheResponse { /// Check if the response is paginated. pub const fn is_paginated(&self) -> bool { - matches!(self, Self::Paginated { .. }) + self.next_cursor.is_some() } /// Check if the response is unpaginated. pub const fn is_unpaginated(&self) -> bool { - matches!(self, Self::Unpaginated { .. }) + self.next_cursor.is_none() } /// Get the inner value. pub fn into_inner(self) -> T { match self { - Self::Paginated { inner, .. } => inner, - Self::Unpaginated { inner } => inner, + Self { inner, .. } => inner, } } /// Consume the response and return the parts. pub fn into_parts(self) -> (T, Option) { match self { - Self::Paginated { inner, next_cursor } => (inner, Some(next_cursor)), - Self::Unpaginated { inner } => (inner, None), + Self { inner, next_cursor } => (inner, next_cursor), } } @@ -889,9 +876,8 @@ mod tests { #[test] fn test_unpaginated_cache_response_deser() { - let cache_response = CacheResponse::Unpaginated { - inner: TxCacheTransactionsResponse { transactions: vec![] }, - }; + let cache_response = + CacheResponse::unpaginated(TxCacheTransactionsResponse { transactions: vec![] }); let expected_json = r#"{"transactions":[]}"#; let serialized = serde_json::to_string(&cache_response).unwrap(); assert_eq!(serialized, expected_json); @@ -903,14 +889,14 @@ mod tests { #[test] fn test_paginated_cache_response_deser() { - let cache_response = CacheResponse::Paginated { - inner: TxCacheTransactionsResponse { transactions: vec![] }, - next_cursor: TxKey { + let cache_response = CacheResponse::paginated( + TxCacheTransactionsResponse { transactions: vec![] }, + TxKey { txn_hash: B256::repeat_byte(0xaa), score: 100, global_transaction_score_key: "gtsk".to_string(), }, - }; + ); let expected_json = r#"{"transactions":[],"nextCursor":{"txnHash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","score":100,"globalTransactionScoreKey":"gtsk"}}"#; let serialized = serde_json::to_string(&cache_response).unwrap(); assert_eq!(serialized, expected_json); @@ -955,13 +941,11 @@ mod tests { #[test] fn test_unpaginated_cache_bundle_response_deser() { - let cache_response = CacheResponse::Unpaginated { - inner: TxCacheBundlesResponse { - bundles: vec![dummy_bundle_with_id( - Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(), - )], - }, - }; + let cache_response = CacheResponse::unpaginated(TxCacheBundlesResponse { + bundles: vec![dummy_bundle_with_id( + Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(), + )], + }); let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}]}"#; let serialized = serde_json::to_string(&cache_response).unwrap(); assert_eq!(serialized, expected_json); @@ -974,14 +958,10 @@ mod tests { fn test_paginated_cache_bundle_response_deser() { let uuid = Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(); - let cache_response = CacheResponse::Paginated { - inner: TxCacheBundlesResponse { bundles: vec![dummy_bundle_with_id(uuid)] }, - next_cursor: BundleKey { - id: uuid, - score: 100, - global_bundle_score_key: "gbsk".to_string(), - }, - }; + let cache_response = CacheResponse::paginated( + TxCacheBundlesResponse { bundles: vec![dummy_bundle_with_id(uuid)] }, + BundleKey { id: uuid, score: 100, global_bundle_score_key: "gbsk".to_string() }, + ); let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}],"nextCursor":{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","score":100,"globalBundleScoreKey":"gbsk"}}"#; let serialized = serde_json::to_string(&cache_response).unwrap(); dbg!(&serialized); From 15b655d6485dac4538b40e50029ebf5694910be1 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 22:04:00 +0100 Subject: [PATCH 31/35] chore: remove dbg --- crates/tx-cache/src/types.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 5c7867da..7c1153a8 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -964,7 +964,6 @@ mod tests { ); let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}],"nextCursor":{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","score":100,"globalBundleScoreKey":"gbsk"}}"#; let serialized = serde_json::to_string(&cache_response).unwrap(); - dbg!(&serialized); assert_eq!(serialized, expected_json); let deserialized = serde_json::from_str::>(&expected_json).unwrap(); From 18ebd0a872198942cf75a84a2dc74f33fe3b978c Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 22:34:28 +0100 Subject: [PATCH 32/35] ? --- crates/tx-cache/src/types.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 7c1153a8..b5d1b33c 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -868,7 +868,6 @@ mod tests { refund_tx_hashes: vec![], extra_fields: Default::default(), }, - host_fills: None, host_txs: vec![], }, } From 09614d635cf821cdbea7a4193908190da71d59ee Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 12 Nov 2025 22:40:58 +0100 Subject: [PATCH 33/35] ?? --- crates/tx-cache/src/types.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index b5d1b33c..8df2b6d7 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -900,7 +900,7 @@ mod tests { let serialized = serde_json::to_string(&cache_response).unwrap(); assert_eq!(serialized, expected_json); let deserialized = - serde_json::from_str::>(&expected_json) + serde_json::from_str::>(expected_json) .unwrap(); assert_eq!(deserialized, cache_response); } @@ -911,7 +911,7 @@ mod tests { fn test_backwards_compatibility_cache_response_deser() { let expected_json = r#"{"transactions":[],"nextCursor":{"txnHash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","score":100,"globalTransactionScoreKey":"gtsk"}}"#; let deserialized = - serde_json::from_str::(&expected_json).unwrap(); + serde_json::from_str::(expected_json).unwrap(); assert_eq!(deserialized, TxCacheTransactionsResponse { transactions: vec![] }); } @@ -922,7 +922,7 @@ mod tests { let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}]}"#; let uuid = Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(); - let deserialized = serde_json::from_str::(&expected_json).unwrap(); + let deserialized = serde_json::from_str::(expected_json).unwrap(); assert_eq!( deserialized, @@ -935,7 +935,7 @@ mod tests { #[test] fn test_backwards_compatibility_cache_order_response_deser() { let expected_json = r#"{"orders":[{"permit":{"permitted":[{"token":"0x0b8bc5e60ee10957e0d1a0d95598fa63e65605e2","amount":"0xf4240"}],"nonce":"0x637253c1eb651","deadline":"0x6846fde6"},"owner":"0x492e9c316f073fe4de9d665221568cdad1a7e95b","signature":"0x73e31a7c80f02840c4e0671230c408a5cbc7cddefc780db4dd102eed8e87c5740fc89944eb8e5756edd368ed755415ed090b043d1740ee6869c20cb1676329621c","outputs":[{"token":"0x885f8db528dc8a38aa3ddad9d3f619746b4a6a81","amount":"0xf4240","recipient":"0x492e9c316f073fe4de9d665221568cdad1a7e95b","chainId":3151908}]}], "id":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}"#; - let _ = serde_json::from_str::(&expected_json).unwrap(); + let _ = serde_json::from_str::(expected_json).unwrap(); } #[test] @@ -949,7 +949,7 @@ mod tests { let serialized = serde_json::to_string(&cache_response).unwrap(); assert_eq!(serialized, expected_json); let deserialized = - serde_json::from_str::>(&expected_json).unwrap(); + serde_json::from_str::>(expected_json).unwrap(); assert_eq!(deserialized, cache_response); } @@ -965,7 +965,7 @@ mod tests { let serialized = serde_json::to_string(&cache_response).unwrap(); assert_eq!(serialized, expected_json); let deserialized = - serde_json::from_str::>(&expected_json).unwrap(); + serde_json::from_str::>(expected_json).unwrap(); assert_eq!(deserialized, cache_response); } From e1e5d638d0aae4bd849455d48ac08e1e9c90a5e8 Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 13 Nov 2025 20:07:22 +0100 Subject: [PATCH 34/35] chore: correct naming --- crates/tx-cache/src/types.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 8df2b6d7..8cc8037f 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -460,7 +460,7 @@ pub struct CursorPayload Deserialize<'a>> { } impl Deserialize<'a>> CursorPayload { - /// Creates a new instance of [`PaginationParams`]. + /// Creates a new instance of [`CursorPayload`]. pub const fn new(cursor: Option) -> Self { Self { cursor } } @@ -470,7 +470,7 @@ impl Deserialize<'a>> CursorPayload { self.cursor.as_ref() } - /// Consumes the [`PaginationParams`] and returns the cursor. + /// Consumes the [`CursorPayload`] and returns the cursor. pub fn into_cursor(self) -> Option { self.cursor } @@ -527,7 +527,7 @@ impl<'de> Deserialize<'de> for CursorPayload { type Value = CursorPayload; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("a PaginationParams") + formatter.write_str("a pagination cursor for the Signet Transaction Cache, of the type `CursorPayload`") } fn visit_seq(self, mut seq: S) -> Result, S::Error> @@ -672,7 +672,7 @@ impl<'de> Deserialize<'de> for CursorPayload { type Value = CursorPayload; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("a PaginationParams") + formatter.write_str("a pagination cursor for the Signet Transaction Cache, of the type `CursorPayload`") } fn visit_seq(self, mut seq: S) -> Result, S::Error> @@ -801,7 +801,7 @@ impl<'de> Deserialize<'de> for CursorPayload { type Value = CursorPayload; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("a PaginationParams") + formatter.write_str("a pagination cursor for the Signet Transaction Cache, of the type `CursorPayload`") } fn visit_seq(self, mut seq: S) -> Result, S::Error> From c83d489926a20aa0eca9f43ec7b80b1c684b6934 Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 13 Nov 2025 20:13:21 +0100 Subject: [PATCH 35/35] chore: move CursorPayload --- crates/tx-cache/src/types.rs | 480 +---------------------------------- 1 file changed, 1 insertion(+), 479 deletions(-) diff --git a/crates/tx-cache/src/types.rs b/crates/tx-cache/src/types.rs index 8cc8037f..089e253c 100644 --- a/crates/tx-cache/src/types.rs +++ b/crates/tx-cache/src/types.rs @@ -1,9 +1,6 @@ //! The endpoints for the transaction cache. use alloy::{consensus::TxEnvelope, primitives::B256}; -use serde::{ - de::{DeserializeOwned, MapAccess, SeqAccess, Visitor}, - Deserialize, Deserializer, Serialize, -}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use signet_bundle::SignetEthBundle; use signet_types::SignedOrder; use uuid::Uuid; @@ -451,401 +448,6 @@ pub struct OrderKey { pub id: B256, } -/// A deserialization helper for cursors keys. -#[derive(Clone, Debug, Serialize)] -pub struct CursorPayload Deserialize<'a>> { - // The cursor key. - #[serde(flatten)] - cursor: Option, -} - -impl Deserialize<'a>> CursorPayload { - /// Creates a new instance of [`CursorPayload`]. - pub const fn new(cursor: Option) -> Self { - Self { cursor } - } - - /// Get the cursor to start from. - pub const fn cursor(&self) -> Option<&C> { - self.cursor.as_ref() - } - - /// Consumes the [`CursorPayload`] and returns the cursor. - pub fn into_cursor(self) -> Option { - self.cursor - } -} - -impl<'de> Deserialize<'de> for CursorPayload { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - enum Field { - TxnHash, - Score, - GlobalTransactionScoreKey, - } - - impl<'de> Deserialize<'de> for Field { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct TxKeyVisitor; - - impl<'de> serde::de::Visitor<'de> for TxKeyVisitor { - type Value = Field; - - fn expecting( - &self, - formatter: &mut std::fmt::Formatter<'_>, - ) -> std::fmt::Result { - formatter.write_str("a TxKeyField") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - match v { - "txnHash" => Ok(Field::TxnHash), - "score" => Ok(Field::Score), - "globalTransactionScoreKey" => Ok(Field::GlobalTransactionScoreKey), - _ => Err(serde::de::Error::unknown_field(v, FIELDS)), - } - } - } - - deserializer.deserialize_str(TxKeyVisitor) - } - } - - struct TxKeyVisitor; - - impl<'de> Visitor<'de> for TxKeyVisitor { - type Value = CursorPayload; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("a pagination cursor for the Signet Transaction Cache, of the type `CursorPayload`") - } - - fn visit_seq(self, mut seq: S) -> Result, S::Error> - where - S: SeqAccess<'de>, - { - // We consider this a complete request if we have no elements in the sequence. - let Some(txn_hash) = seq.next_element()? else { - // We consider this a complete request if we have no txn hash. - return Ok(CursorPayload::new(None)); - }; - - // For all other items, we require a score and a global transaction score key. - let score = seq - .next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - let global_transaction_score_key = seq - .next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; - Ok(CursorPayload::new(Some(TxKey { - txn_hash, - score, - global_transaction_score_key, - }))) - } - - fn visit_map(self, mut map: M) -> Result, M::Error> - where - M: MapAccess<'de>, - { - let mut txn_hash = None; - let mut score = None; - let mut global_transaction_score_key = None; - - while let Some(key) = map.next_key()? { - match key { - Field::TxnHash => { - if txn_hash.is_some() { - return Err(serde::de::Error::duplicate_field("txnHash")); - } - txn_hash = Some(map.next_value()?); - } - Field::Score => { - if score.is_some() { - return Err(serde::de::Error::duplicate_field("score")); - } - score = Some(map.next_value()?); - } - Field::GlobalTransactionScoreKey => { - if global_transaction_score_key.is_some() { - return Err(serde::de::Error::duplicate_field( - "globalTransactionScoreKey", - )); - } - global_transaction_score_key = Some(map.next_value()?); - } - } - } - - // We consider this a complete request if we have no txn hash and no other fields are present. - let txn_hash = match txn_hash { - Some(hash) => hash, - None => { - if score.is_some() || global_transaction_score_key.is_some() { - return Err(serde::de::Error::invalid_length( - score.is_some() as usize - + global_transaction_score_key.is_some() as usize, - &self, - )); - } - return Ok(CursorPayload::new(None)); - } - }; - - // For all other items, we require a score and a global transaction score key. - let score = score.ok_or_else(|| serde::de::Error::missing_field("score"))?; - let global_transaction_score_key = global_transaction_score_key - .ok_or_else(|| serde::de::Error::missing_field("globalTransactionScoreKey"))?; - - Ok(CursorPayload::new(Some(TxKey { - txn_hash, - score, - global_transaction_score_key, - }))) - } - } - - const FIELDS: &[&str] = &["txnHash", "score", "globalTransactionScoreKey"]; - deserializer.deserialize_struct("TxKey", FIELDS, TxKeyVisitor) - } -} - -impl<'de> Deserialize<'de> for CursorPayload { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - { - enum Field { - Id, - Score, - GlobalBundleScoreKey, - } - - impl<'de> Deserialize<'de> for Field { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct BundleKeyVisitor; - - impl<'de> serde::de::Visitor<'de> for BundleKeyVisitor { - type Value = Field; - - fn expecting( - &self, - formatter: &mut std::fmt::Formatter<'_>, - ) -> std::fmt::Result { - formatter.write_str("a BundleKeyField") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - match v { - "id" => Ok(Field::Id), - "score" => Ok(Field::Score), - "globalBundleScoreKey" => Ok(Field::GlobalBundleScoreKey), - _ => Err(serde::de::Error::unknown_field(v, FIELDS)), - } - } - } - - deserializer.deserialize_str(BundleKeyVisitor) - } - } - - struct BundleKeyVisitor; - - impl<'de> Visitor<'de> for BundleKeyVisitor { - type Value = CursorPayload; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("a pagination cursor for the Signet Transaction Cache, of the type `CursorPayload`") - } - - fn visit_seq(self, mut seq: S) -> Result, S::Error> - where - S: SeqAccess<'de>, - { - // We consider this a complete request if we have no elements in the sequence. - let Some(id) = seq.next_element()? else { - return Ok(CursorPayload::new(None)); - }; - - // For all other items, we require a score and a global transaction score key. - let score = seq - .next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - let global_bundle_score_key = seq - .next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; - Ok(CursorPayload::new(Some(BundleKey { id, score, global_bundle_score_key }))) - } - - fn visit_map(self, mut map: M) -> Result, M::Error> - where - M: MapAccess<'de>, - { - let mut id = None; - let mut score = None; - let mut global_bundle_score_key = None; - - while let Some(key) = map.next_key()? { - match key { - Field::Id => { - if id.is_some() { - return Err(serde::de::Error::duplicate_field("id")); - } - id = Some(map.next_value()?); - } - Field::Score => { - if score.is_some() { - return Err(serde::de::Error::duplicate_field("score")); - } - score = Some(map.next_value()?); - } - Field::GlobalBundleScoreKey => { - if global_bundle_score_key.is_some() { - return Err(serde::de::Error::duplicate_field( - "globalBundleScoreKey", - )); - } - global_bundle_score_key = Some(map.next_value()?); - } - } - } - - // We consider this a complete request if we have no id and no other fields are present. - let Some(id) = id else { - if score.is_some() || global_bundle_score_key.is_some() { - return Err(serde::de::Error::invalid_length( - score.is_some() as usize - + global_bundle_score_key.is_some() as usize, - &self, - )); - } - return Ok(CursorPayload::new(None)); - }; - - // For all other items, we require a score and a global bundle score key. - let score = score.ok_or_else(|| serde::de::Error::missing_field("score"))?; - let global_bundle_score_key = global_bundle_score_key - .ok_or_else(|| serde::de::Error::missing_field("globalBundleScoreKey"))?; - Ok(CursorPayload::new(Some(BundleKey { id, score, global_bundle_score_key }))) - } - } - - const FIELDS: &[&str] = &["id", "score", "globalBundleScoreKey"]; - deserializer.deserialize_struct("BundleKey", FIELDS, BundleKeyVisitor) - } - } -} - -impl<'de> Deserialize<'de> for CursorPayload { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - { - enum Field { - Id, - } - - impl<'de> Deserialize<'de> for Field { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct OrderKeyVisitor; - - impl<'de> serde::de::Visitor<'de> for OrderKeyVisitor { - type Value = Field; - - fn expecting( - &self, - formatter: &mut std::fmt::Formatter<'_>, - ) -> std::fmt::Result { - formatter.write_str("a OrderKeyField") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - match v { - "id" => Ok(Field::Id), - _ => Err(serde::de::Error::unknown_field(v, FIELDS)), - } - } - } - - deserializer.deserialize_str(OrderKeyVisitor) - } - } - - struct OrderKeyVisitor; - - impl<'de> Visitor<'de> for OrderKeyVisitor { - type Value = CursorPayload; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("a pagination cursor for the Signet Transaction Cache, of the type `CursorPayload`") - } - - fn visit_seq(self, mut seq: S) -> Result, S::Error> - where - S: SeqAccess<'de>, - { - let Some(id) = seq.next_element()? else { - return Ok(CursorPayload::new(None)); - }; - - Ok(CursorPayload::new(Some(OrderKey { id }))) - } - - fn visit_map(self, mut map: M) -> Result, M::Error> - where - M: MapAccess<'de>, - { - let mut id = None; - - while let Some(key) = map.next_key()? { - match key { - Field::Id => { - if id.is_some() { - return Err(serde::de::Error::duplicate_field("id")); - } - id = Some(map.next_value()?); - } - } - } - - let Some(id) = id else { - return Ok(CursorPayload::new(None)); - }; - - Ok(CursorPayload::new(Some(OrderKey { id }))) - } - } - - const FIELDS: &[&str] = &["id"]; - deserializer.deserialize_struct("OrderKey", FIELDS, OrderKeyVisitor) - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -984,84 +586,4 @@ mod tests { assert_eq!(serialized, "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100&globalTransactionScoreKey=gtsk"); assert_eq!(empty_serialized, ""); } - - #[test] - fn test_pagination_params_partial_deser() { - let tx_key = TxKey { - txn_hash: B256::repeat_byte(0xaa), - score: 100, - global_transaction_score_key: "gtsk".to_string(), - }; - let serialized = serde_urlencoded::to_string(&tx_key).unwrap(); - assert_eq!(serialized, "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100&globalTransactionScoreKey=gtsk"); - - let deserialized = serde_urlencoded::from_str::>(&serialized).unwrap(); - assert_eq!(deserialized.cursor().unwrap(), &tx_key); - - let partial_query_string = "score=100&globalTransactionScoreKey=gtsk"; - let partial_params = - serde_urlencoded::from_str::>(partial_query_string); - assert!(partial_params.is_err()); - - let partial_query_string = - "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100"; - let partial_params = - serde_urlencoded::from_str::>(partial_query_string); - assert!(partial_params.is_err()); - - let empty_query_string = ""; - let empty_params = serde_urlencoded::from_str::>(empty_query_string); - assert!(empty_params.is_ok()); - assert!(empty_params.unwrap().cursor().is_none()); - } - - #[test] - fn test_pagination_params_bundle_deser() { - let bundle_key = BundleKey { - // This is our UUID. Nobody else use it. - id: Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(), - score: 100, - global_bundle_score_key: "gbsk".to_string(), - }; - - let serialized = serde_urlencoded::to_string(&bundle_key).unwrap(); - assert_eq!( - serialized, - "id=5932d4bb-58d9-41a9-851d-8dd7f04ccc33&score=100&globalBundleScoreKey=gbsk" - ); - - let deserialized = - serde_urlencoded::from_str::>(&serialized).unwrap(); - assert_eq!(deserialized.cursor().unwrap(), &bundle_key); - - let partial_query_string = "score=100&globalBundleScoreKey=gbsk"; - let partial_params = - serde_urlencoded::from_str::>(partial_query_string); - assert!(partial_params.is_err()); - - let partial_query_string = "id=5932d4bb-58d9-41a9-851d-8dd7f04ccc33&score=100"; - let partial_params = - serde_urlencoded::from_str::>(partial_query_string); - assert!(partial_params.is_err()); - - let empty_query_string = ""; - let empty_params = - serde_urlencoded::from_str::>(empty_query_string); - assert!(empty_params.is_ok()); - assert!(empty_params.unwrap().cursor().is_none()); - } - - #[test] - fn test_pagination_params_order_deser() { - let order_key = OrderKey { id: B256::repeat_byte(0xaa) }; - let serialized = serde_urlencoded::to_string(order_key).unwrap(); - assert_eq!( - serialized, - "id=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ); - - let deserialized = - serde_urlencoded::from_str::>(&serialized).unwrap(); - assert_eq!(deserialized.cursor().unwrap(), &order_key); - } }