signet_tx_cache/
types.rs

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