switchgear_components/offer/
provider.rs

1use crate::offer::error::OfferStoreError;
2use async_trait::async_trait;
3use sha2::{Digest, Sha256};
4use switchgear_service_api::lnurl::LnUrlOfferMetadata;
5use switchgear_service_api::offer::{Offer, OfferProvider, OfferStore};
6use switchgear_service_api::service::ServiceErrorSource;
7use uuid::Uuid;
8
9#[derive(Clone, Debug)]
10pub struct StoreOfferProvider<S> {
11    store: S,
12}
13
14impl<S> StoreOfferProvider<S> {
15    pub fn new(store: S) -> Self {
16        Self { store }
17    }
18}
19
20#[async_trait]
21impl<S> OfferProvider for StoreOfferProvider<S>
22where
23    S: OfferStore + Send + Sync + 'static,
24    S::Error: From<OfferStoreError>,
25{
26    type Error = S::Error;
27
28    async fn offer(
29        &self,
30        _hostname: &str,
31        partition: &str,
32        id: &Uuid,
33    ) -> Result<Option<Offer>, Self::Error> {
34        if let Some(offer) = self.store.get_offer(partition, id, Some(false)).await? {
35            let offer_metadata = match offer.offer.metadata {
36                Some(metadata) => metadata,
37                None => {
38                    return Ok(None);
39                }
40            };
41
42            let lnurl_metadata = LnUrlOfferMetadata(offer_metadata);
43            let metadata_json_string = serde_json::to_string(&lnurl_metadata).map_err(|e| {
44                OfferStoreError::serialization_error(
45                    ServiceErrorSource::Internal,
46                    format!("building LNURL offer response for offer {}", offer.id),
47                    e,
48                )
49            })?;
50
51            let mut hasher = Sha256::new();
52            hasher.update(metadata_json_string.as_bytes());
53            let metadata_json_hash = hasher.finalize().into();
54
55            Ok(Some(Offer {
56                partition: offer.partition,
57                id: offer.id,
58                max_sendable: offer.offer.max_sendable,
59                min_sendable: offer.offer.min_sendable,
60                metadata_json_string,
61                metadata_json_hash,
62                timestamp: offer.offer.timestamp,
63                expires: offer.offer.expires,
64            }))
65        } else {
66            Ok(None)
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use chrono::Utc;
75    use switchgear_service_api::offer::{
76        OfferMetadataIdentifier, OfferMetadataImage, OfferMetadataSparse, OfferRecord,
77        OfferRecordSparse,
78    };
79
80    // Mock OfferStore for testing
81    #[derive(Clone)]
82    struct MockOfferStore {
83        response: Option<OfferRecord>,
84    }
85
86    impl MockOfferStore {
87        fn new(response: Option<OfferRecord>) -> Self {
88            Self { response }
89        }
90    }
91
92    #[async_trait]
93    impl OfferStore for MockOfferStore {
94        type Error = OfferStoreError;
95
96        async fn get_offer(
97            &self,
98            _partition: &str,
99            _id: &Uuid,
100            _sparse: Option<bool>,
101        ) -> Result<Option<OfferRecord>, Self::Error> {
102            Ok(self.response.clone())
103        }
104
105        async fn get_offers(
106            &self,
107            _partition: &str,
108            _start: usize,
109            _count: usize,
110        ) -> Result<Vec<OfferRecord>, Self::Error> {
111            Ok(vec![])
112        }
113
114        async fn post_offer(&self, _offer: OfferRecord) -> Result<Option<Uuid>, Self::Error> {
115            Ok(None)
116        }
117
118        async fn put_offer(&self, _offer: OfferRecord) -> Result<bool, Self::Error> {
119            Ok(false)
120        }
121
122        async fn delete_offer(&self, _partition: &str, _id: &Uuid) -> Result<bool, Self::Error> {
123            Ok(false)
124        }
125    }
126
127    // Test data generator
128    fn create_offer_with_metadata(offer_id: Uuid, metadata_id: Uuid) -> OfferRecord {
129        OfferRecord {
130            partition: "default".to_string(),
131            id: offer_id,
132            offer: OfferRecordSparse {
133                max_sendable: 5000000,
134                min_sendable: 1000,
135                metadata_id,
136                metadata: Some(OfferMetadataSparse {
137                    text: "Test LNURL offer".to_string(),
138                    long_text: Some(
139                        "This is a comprehensive test of the LNURL offer system".to_string(),
140                    ),
141                    image: Some(OfferMetadataImage::Png(vec![0x89, 0x50, 0x4E, 0x47])),
142                    identifier: Some(OfferMetadataIdentifier::Email(
143                        "test@lnurl.com".parse().unwrap(),
144                    )),
145                }),
146                timestamp: Utc::now(),
147                expires: Some(Utc::now() + chrono::Duration::hours(24)),
148            },
149        }
150    }
151
152    #[tokio::test]
153    async fn test_offer_provider_successful_retrieval() {
154        let offer_id = Uuid::new_v4();
155        let metadata_id = Uuid::new_v4();
156        let offer = create_offer_with_metadata(offer_id, metadata_id);
157
158        let store = MockOfferStore::new(Some(offer));
159        let provider = StoreOfferProvider::new(store);
160        let result = provider
161            .offer("example.com", "default", &offer_id)
162            .await
163            .unwrap();
164
165        assert!(result.is_some());
166        let offer = result.unwrap();
167
168        // Verify basic offer fields
169        assert_eq!(offer.id, offer_id);
170        assert_eq!(offer.max_sendable, 5000000);
171        assert_eq!(offer.min_sendable, 1000);
172
173        // Verify metadata_json_string is in LNURL format and contains the expected metadata
174        assert!(offer.metadata_json_string.starts_with("["));
175        assert!(offer.metadata_json_string.contains("Test LNURL offer"));
176        assert!(offer.metadata_json_string.contains("test@lnurl.com"));
177
178        // Verify the JSON string can be deserialized back to LnUrlOfferMetadata
179        let parsed_metadata: LnUrlOfferMetadata =
180            serde_json::from_str(&offer.metadata_json_string).unwrap();
181        assert_eq!(parsed_metadata.0.text, "Test LNURL offer");
182        assert_eq!(
183            parsed_metadata.0.long_text,
184            Some("This is a comprehensive test of the LNURL offer system".to_string())
185        );
186
187        // Verify hash is calculated correctly
188        let expected_hash = sha2::Sha256::digest(offer.metadata_json_string.as_bytes());
189        assert_eq!(offer.metadata_json_hash, expected_hash.as_ref());
190    }
191
192    #[tokio::test]
193    async fn test_offer_provider_offer_not_found() {
194        let store = MockOfferStore::new(None);
195        let provider = StoreOfferProvider::new(store);
196
197        let non_existent_id = Uuid::new_v4();
198        let result = provider
199            .offer("example.com", "default", &non_existent_id)
200            .await
201            .unwrap();
202
203        assert!(result.is_none());
204    }
205}