Skip to main content

signet_tx_cache/
types.rs

1//! The endpoints for the transaction cache.
2use alloy::{consensus::TxEnvelope, primitives::B256};
3use core::ops::{Deref, DerefMut};
4use serde::{de::DeserializeOwned, Deserialize, Serialize};
5use signet_bundle::SignetEthBundle;
6use signet_types::SignedOrder;
7use uuid::Uuid;
8
9/// A trait for types that can be used as a cache object.
10pub trait CacheObject {
11    /// The cursor key type for the cache object.
12    type Key: Serialize + DeserializeOwned;
13}
14
15/// A response from the transaction cache, containing an item.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18pub struct CacheResponse<T: CacheObject> {
19    /// The response.
20    #[serde(flatten)]
21    inner: T,
22    /// The next cursor for pagination, if any.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    next_cursor: Option<T::Key>,
25}
26
27impl<T: CacheObject> CacheObject for CacheResponse<T> {
28    type Key = T::Key;
29}
30
31impl<T: CacheObject> Deref for CacheResponse<T> {
32    type Target = T;
33
34    fn deref(&self) -> &Self::Target {
35        &self.inner
36    }
37}
38
39impl<T: CacheObject> DerefMut for CacheResponse<T> {
40    fn deref_mut(&mut self) -> &mut Self::Target {
41        &mut self.inner
42    }
43}
44
45impl<T: CacheObject> CacheResponse<T> {
46    /// Create a new paginated response from a list of items and a pagination info.
47    pub const fn paginated(inner: T, pagination: T::Key) -> Self {
48        Self { inner, next_cursor: Some(pagination) }
49    }
50
51    /// Create a new unpaginated response from a list of items.
52    pub const fn unpaginated(inner: T) -> Self {
53        Self { inner, next_cursor: None }
54    }
55
56    /// Return a reference to the inner value.
57    #[deprecated = "use deref instead"]
58    pub const fn inner(&self) -> &T {
59        match self {
60            Self { inner, .. } => inner,
61        }
62    }
63
64    /// Return a mutable reference to the inner value.
65    #[deprecated = "use deref_mut instead"]
66    pub const fn inner_mut(&mut self) -> &mut T {
67        match self {
68            Self { inner, .. } => inner,
69        }
70    }
71
72    /// Return the next cursor for pagination, if any.
73    pub const fn next_cursor(&self) -> Option<&T::Key> {
74        match self {
75            Self { next_cursor, .. } => next_cursor.as_ref(),
76        }
77    }
78
79    /// Check if the response has more items to fetch.
80    pub const fn has_more(&self) -> bool {
81        self.next_cursor().is_some()
82    }
83
84    /// Check if the response is paginated.
85    pub const fn is_paginated(&self) -> bool {
86        self.next_cursor.is_some()
87    }
88
89    /// Check if the response is unpaginated.
90    pub const fn is_unpaginated(&self) -> bool {
91        self.next_cursor.is_none()
92    }
93
94    /// Get the inner value.
95    pub fn into_inner(self) -> T {
96        match self {
97            Self { inner, .. } => inner,
98        }
99    }
100
101    /// Consume the response and return the parts.
102    pub fn into_parts(self) -> (T, Option<T::Key>) {
103        match self {
104            Self { inner, next_cursor } => (inner, next_cursor),
105        }
106    }
107
108    /// Consume the response and return the next cursor for pagination, if any.
109    pub fn into_next_cursor(self) -> Option<T::Key> {
110        self.into_parts().1
111    }
112}
113
114/// A bundle stored in the cache with its ID.
115///
116/// Contains a UUID identifier and the underlying [`SignetEthBundle`].
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118pub struct CachedBundle {
119    /// The bundle id (a UUID)
120    pub id: uuid::Uuid,
121    /// The bundle itself
122    pub bundle: SignetEthBundle,
123}
124
125impl CachedBundle {
126    /// Create a new cached bundle from a bundle and an id.
127    pub const fn new(bundle: SignetEthBundle, id: uuid::Uuid) -> Self {
128        Self { id, bundle }
129    }
130
131    /// Create a new cached bundle from a bundle and an id.
132    #[deprecated = "Use `Self::new` instead"]
133    pub const fn from_bundle_and_id(bundle: SignetEthBundle, id: uuid::Uuid) -> Self {
134        Self::new(bundle, id)
135    }
136
137    /// Convert the cached bundle to a [`SignetEthBundle`].
138    pub fn into_bundle(self) -> SignetEthBundle {
139        self.bundle
140    }
141
142    /// Convert the cached bundle to a [uuid::Uuid].
143    pub fn into_id(self) -> uuid::Uuid {
144        self.id
145    }
146
147    /// The bundle id.
148    pub const fn id(&self) -> uuid::Uuid {
149        self.id
150    }
151
152    /// The bundle itself.
153    pub const fn bundle(&self) -> &SignetEthBundle {
154        &self.bundle
155    }
156}
157
158/// A collection of cached bundles.
159///
160/// Returned by the transaction cache `bundles` endpoint.
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
162pub struct BundleList {
163    /// the list of bundles
164    pub bundles: Vec<CachedBundle>,
165}
166
167impl From<Vec<CachedBundle>> for BundleList {
168    fn from(bundles: Vec<CachedBundle>) -> Self {
169        Self { bundles }
170    }
171}
172
173impl From<BundleList> for Vec<CachedBundle> {
174    fn from(response: BundleList) -> Self {
175        response.bundles
176    }
177}
178
179impl CacheObject for BundleList {
180    type Key = BundleKey;
181}
182
183impl BundleList {
184    /// Create a new bundle list from a list of bundles.
185    pub const fn new(bundles: Vec<CachedBundle>) -> Self {
186        Self { bundles }
187    }
188
189    /// Create a new bundle list from a list of bundles.
190    #[deprecated = "Use `From::from` instead, `Self::new` in const contexts"]
191    pub const fn from_bundles(bundles: Vec<CachedBundle>) -> Self {
192        Self { bundles }
193    }
194
195    /// Convert the bundle list to a list of [`CachedBundle`].
196    #[deprecated = "Use `this.bundles` instead."]
197    pub fn into_bundles(self) -> Vec<CachedBundle> {
198        self.bundles
199    }
200
201    /// Check if the list is empty (has no bundles).
202    pub const fn is_empty(&self) -> bool {
203        self.bundles.is_empty()
204    }
205}
206
207/// Acknowledgment after submitting a bundle to the transaction cache.
208///
209/// Contains the UUID assigned to the submitted bundle.
210#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
211pub struct BundleResponse {
212    /// The bundle id (a UUID)
213    pub id: uuid::Uuid,
214}
215
216impl BundleResponse {
217    /// Create a new bundle response from a bundle id.
218    pub const fn new(id: uuid::Uuid) -> Self {
219        Self { id }
220    }
221}
222
223impl From<uuid::Uuid> for BundleResponse {
224    fn from(id: uuid::Uuid) -> Self {
225        Self { id }
226    }
227}
228
229impl From<BundleResponse> for uuid::Uuid {
230    fn from(response: BundleResponse) -> Self {
231        response.id
232    }
233}
234
235impl CacheObject for BundleResponse {
236    type Key = BundleKey;
237}
238
239/// A collection of transactions from the cache.
240///
241/// Returned by the transaction cache `transactions` endpoint.
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
243pub struct TransactionList {
244    /// The list of transactions.
245    pub transactions: Vec<TxEnvelope>,
246}
247
248impl From<Vec<TxEnvelope>> for TransactionList {
249    fn from(transactions: Vec<TxEnvelope>) -> Self {
250        Self { transactions }
251    }
252}
253
254impl From<TransactionList> for Vec<TxEnvelope> {
255    fn from(response: TransactionList) -> Self {
256        response.transactions
257    }
258}
259
260impl CacheObject for TransactionList {
261    type Key = TxKey;
262}
263
264impl TransactionList {
265    /// Instantiate a new transaction list from a list of transactions.
266    pub const fn new(transactions: Vec<TxEnvelope>) -> Self {
267        Self { transactions }
268    }
269
270    /// Create a new transaction list from a list of transactions.
271    #[deprecated = "Use `From::from` instead, or `Self::new` in const contexts"]
272    pub const fn from_transactions(transactions: Vec<TxEnvelope>) -> Self {
273        Self { transactions }
274    }
275
276    /// Convert the transaction list to a list of [`TxEnvelope`].
277    #[deprecated = "Use `this.transactions` instead."]
278    pub fn into_transactions(self) -> Vec<TxEnvelope> {
279        self.transactions
280    }
281
282    /// Check if the list is empty (has no transactions).
283    pub const fn is_empty(&self) -> bool {
284        self.transactions.is_empty()
285    }
286}
287
288/// Acknowledgment after submitting a transaction to the cache.
289///
290/// Contains the transaction hash of the submitted transaction.
291#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
292pub struct TransactionResponse {
293    /// The transaction hash
294    pub tx_hash: B256,
295}
296
297impl From<B256> for TransactionResponse {
298    fn from(tx_hash: B256) -> Self {
299        Self { tx_hash }
300    }
301}
302
303impl From<TransactionResponse> for B256 {
304    fn from(response: TransactionResponse) -> Self {
305        response.tx_hash
306    }
307}
308
309impl CacheObject for TransactionResponse {
310    type Key = TxKey;
311}
312
313impl TransactionResponse {
314    /// Create a new transaction response from a transaction hash.
315    pub const fn new(tx_hash: B256) -> Self {
316        Self { tx_hash }
317    }
318
319    /// Create a new transaction response from a transaction hash.
320    #[deprecated = "Use `From::from` instead, or `Self::new` in const contexts"]
321    pub const fn from_tx_hash(tx_hash: B256) -> Self {
322        Self { tx_hash }
323    }
324
325    /// Convert the transaction response to a transaction hash.
326    #[deprecated = "Use `this.tx_hash` instead."]
327    pub const fn into_tx_hash(self) -> B256 {
328        self.tx_hash
329    }
330}
331
332/// A collection of signed orders from the cache.
333///
334/// Returned by the transaction cache `orders` endpoint.
335#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
336pub struct OrderList {
337    /// The list of signed orders.
338    pub orders: Vec<SignedOrder>,
339}
340
341impl From<Vec<SignedOrder>> for OrderList {
342    fn from(orders: Vec<SignedOrder>) -> Self {
343        Self { orders }
344    }
345}
346
347impl From<OrderList> for Vec<SignedOrder> {
348    fn from(response: OrderList) -> Self {
349        response.orders
350    }
351}
352
353impl CacheObject for OrderList {
354    type Key = OrderKey;
355}
356
357impl OrderList {
358    /// Create a new order list from a list of orders.
359    pub const fn new(orders: Vec<SignedOrder>) -> Self {
360        Self { orders }
361    }
362
363    /// Create a new order list from a list of orders.
364    #[deprecated = "Use `From::from` instead, `Self::new` in const contexts"]
365    pub const fn from_orders(orders: Vec<SignedOrder>) -> Self {
366        Self { orders }
367    }
368
369    /// Convert the order list to a list of [`SignedOrder`].
370    #[deprecated = "Use `this.orders` instead."]
371    pub fn into_orders(self) -> Vec<SignedOrder> {
372        self.orders
373    }
374}
375
376/// Acknowledgment after submitting an order to the cache.
377///
378/// Contains the order ID of the submitted order.
379#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
380pub struct OrderResponse {
381    /// The order id
382    pub id: B256,
383}
384
385impl From<B256> for OrderResponse {
386    fn from(id: B256) -> Self {
387        Self { id }
388    }
389}
390
391impl From<OrderResponse> for B256 {
392    fn from(response: OrderResponse) -> Self {
393        response.id
394    }
395}
396
397impl CacheObject for OrderResponse {
398    type Key = OrderKey;
399}
400
401impl OrderResponse {
402    /// Create a new order response from an order id.
403    pub const fn new(id: B256) -> Self {
404        Self { id }
405    }
406}
407
408/// The query object keys for the transaction GET endpoint.
409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
410#[serde(rename_all = "camelCase")]
411pub struct TxKey {
412    /// The transaction hash
413    pub txn_hash: B256,
414    /// The transaction score
415    pub score: u64,
416    /// The global transaction score key
417    pub global_transaction_score_key: String,
418}
419
420/// The query object keys for the bundle GET endpoint.
421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
422#[serde(rename_all = "camelCase")]
423pub struct BundleKey {
424    /// The bundle id
425    pub id: Uuid,
426    /// The bundle score
427    pub score: u64,
428    /// The global bundle score key
429    pub global_bundle_score_key: String,
430}
431
432/// The query object keys for the order GET endpoint.
433#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
434pub struct OrderKey {
435    /// The order id
436    pub id: B256,
437}
438
439// ============================================================================
440// Deprecated type aliases for backwards compatibility
441// ============================================================================
442
443#[deprecated(since = "0.16.0", note = "renamed to `CachedBundle`")]
444/// Deprecated alias for [`CachedBundle`].
445pub type TxCacheBundle = CachedBundle;
446
447#[deprecated(since = "0.16.0", note = "renamed to `BundleList`")]
448/// Deprecated alias for [`BundleList`].
449pub type TxCacheBundlesResponse = BundleList;
450
451#[deprecated(since = "0.16.0", note = "renamed to `BundleResponse`")]
452/// Deprecated alias for [`BundleResponse`].
453pub type BundleReceipt = BundleResponse;
454
455#[deprecated(since = "0.16.0", note = "renamed to `BundleResponse`")]
456/// Deprecated alias for [`BundleResponse`].
457pub type TxCacheSendBundleResponse = BundleResponse;
458
459#[deprecated(since = "0.16.0", note = "renamed to `TransactionList`")]
460/// Deprecated alias for [`TransactionList`].
461pub type TxCacheTransactionsResponse = TransactionList;
462
463#[deprecated(since = "0.16.0", note = "renamed to `TransactionResponse`")]
464/// Deprecated alias for [`TransactionResponse`].
465pub type TransactionReceipt = TransactionResponse;
466
467#[deprecated(since = "0.16.0", note = "renamed to `TransactionResponse`")]
468/// Deprecated alias for [`TransactionResponse`].
469pub type TxCacheSendTransactionResponse = TransactionResponse;
470
471#[deprecated(since = "0.16.0", note = "renamed to `OrderList`")]
472/// Deprecated alias for [`OrderList`].
473pub type TxCacheOrdersResponse = OrderList;
474
475#[deprecated(since = "0.16.0", note = "renamed to `OrderResponse`")]
476/// Deprecated alias for [`OrderResponse`].
477pub type OrderReceipt = OrderResponse;
478
479#[deprecated(since = "0.16.0", note = "renamed to `OrderResponse`")]
480/// Deprecated alias for [`OrderResponse`].
481pub type TxCacheSendOrderResponse = OrderResponse;
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    use std::str::FromStr;
487
488    fn dummy_bundle_with_id(id: Uuid) -> CachedBundle {
489        CachedBundle {
490            id,
491            bundle: SignetEthBundle {
492                bundle: alloy::rpc::types::mev::EthSendBundle {
493                    txs: vec![],
494                    block_number: 0,
495                    min_timestamp: None,
496                    max_timestamp: None,
497                    reverting_tx_hashes: vec![],
498                    replacement_uuid: Some(id.to_string()),
499                    dropping_tx_hashes: vec![],
500                    refund_percent: None,
501                    refund_recipient: None,
502                    refund_tx_hashes: vec![],
503                    extra_fields: Default::default(),
504                },
505                host_txs: vec![],
506            },
507        }
508    }
509
510    #[test]
511    fn test_unpaginated_cache_response_deser() {
512        let cache_response = CacheResponse::unpaginated(TransactionList { transactions: vec![] });
513        let expected_json = r#"{"transactions":[]}"#;
514        let serialized = serde_json::to_string(&cache_response).unwrap();
515        assert_eq!(serialized, expected_json);
516        let deserialized =
517            serde_json::from_str::<CacheResponse<TransactionList>>(&serialized).unwrap();
518        assert_eq!(deserialized, cache_response);
519    }
520
521    #[test]
522    fn test_paginated_cache_response_deser() {
523        let cache_response = CacheResponse::paginated(
524            TransactionList { transactions: vec![] },
525            TxKey {
526                txn_hash: B256::repeat_byte(0xaa),
527                score: 100,
528                global_transaction_score_key: "gtsk".to_string(),
529            },
530        );
531        let expected_json = r#"{"transactions":[],"nextCursor":{"txnHash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","score":100,"globalTransactionScoreKey":"gtsk"}}"#;
532        let serialized = serde_json::to_string(&cache_response).unwrap();
533        assert_eq!(serialized, expected_json);
534        let deserialized =
535            serde_json::from_str::<CacheResponse<TransactionList>>(expected_json).unwrap();
536        assert_eq!(deserialized, cache_response);
537    }
538
539    // `serde_json` should be able to deserialize the old format, regardless if there's pagination information on the response.
540    // This mimics the behavior of the types pre-pagination.
541    #[test]
542    fn test_backwards_compatibility_cache_response_deser() {
543        let expected_json = r#"{"transactions":[],"nextCursor":{"txnHash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","score":100,"globalTransactionScoreKey":"gtsk"}}"#;
544        let deserialized = serde_json::from_str::<TransactionList>(expected_json).unwrap();
545        assert_eq!(deserialized, TransactionList { transactions: vec![] });
546    }
547
548    // `serde_json` should be able to deserialize the old format, regardless if there's pagination information on the response.
549    // This mimics the behavior of the types pre-pagination.
550    #[test]
551    fn test_backwards_compatibility_cache_bundle_response_deser() {
552        let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}]}"#;
553        let uuid = Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap();
554
555        let deserialized = serde_json::from_str::<BundleList>(expected_json).unwrap();
556
557        assert_eq!(deserialized, BundleList { bundles: vec![dummy_bundle_with_id(uuid)] });
558    }
559
560    // `serde_json` should be able to deserialize the old format, regardless if there's pagination information on the response.
561    // This mimics the behavior of the types pre-pagination.
562    #[test]
563    fn test_backwards_compatibility_cache_order_response_deser() {
564        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"}"#;
565        let _ = serde_json::from_str::<OrderList>(expected_json).unwrap();
566    }
567
568    #[test]
569    fn test_unpaginated_cache_bundle_response_deser() {
570        let cache_response = CacheResponse::unpaginated(BundleList {
571            bundles: vec![dummy_bundle_with_id(
572                Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap(),
573            )],
574        });
575        let expected_json = r#"{"bundles":[{"id":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33","bundle":{"txs":[],"blockNumber":"0x0","replacementUuid":"5932d4bb-58d9-41a9-851d-8dd7f04ccc33"}}]}"#;
576        let serialized = serde_json::to_string(&cache_response).unwrap();
577        assert_eq!(serialized, expected_json);
578        let deserialized =
579            serde_json::from_str::<CacheResponse<BundleList>>(expected_json).unwrap();
580        assert_eq!(deserialized, cache_response);
581    }
582
583    #[test]
584    fn test_paginated_cache_bundle_response_deser() {
585        let uuid = Uuid::from_str("5932d4bb-58d9-41a9-851d-8dd7f04ccc33").unwrap();
586
587        let cache_response = CacheResponse::paginated(
588            BundleList { bundles: vec![dummy_bundle_with_id(uuid)] },
589            BundleKey { id: uuid, score: 100, global_bundle_score_key: "gbsk".to_string() },
590        );
591        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"}}"#;
592        let serialized = serde_json::to_string(&cache_response).unwrap();
593        assert_eq!(serialized, expected_json);
594        let deserialized =
595            serde_json::from_str::<CacheResponse<BundleList>>(expected_json).unwrap();
596        assert_eq!(deserialized, cache_response);
597    }
598
599    #[test]
600    fn test_pagination_params_simple_deser() {
601        let tx_key = TxKey {
602            txn_hash: B256::repeat_byte(0xaa),
603            score: 100,
604            global_transaction_score_key: "gtsk".to_string(),
605        };
606        let params = tx_key.clone();
607        let empty_params: Option<TxKey> = None;
608
609        let serialized = serde_urlencoded::to_string(&params).unwrap();
610        let empty_serialized = serde_urlencoded::to_string(&empty_params).unwrap();
611        assert_eq!(serialized, "txnHash=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&score=100&globalTransactionScoreKey=gtsk");
612        assert_eq!(empty_serialized, "");
613    }
614
615    #[test]
616    fn test_cache_response_deref() {
617        let uuid = Uuid::new_v4();
618        let response =
619            CacheResponse::unpaginated(BundleList::new(vec![dummy_bundle_with_id(uuid)]));
620
621        assert_eq!(response.bundles.len(), 1);
622        assert_eq!(response.bundles[0].id, uuid);
623    }
624
625    #[test]
626    fn test_cache_response_deref_mut() {
627        let uuid = Uuid::new_v4();
628        let mut response =
629            CacheResponse::unpaginated(BundleList::new(vec![dummy_bundle_with_id(uuid)]));
630
631        response.bundles.clear();
632        assert!(response.bundles.is_empty());
633    }
634}