switchgear_components/offer/
memory.rs

1use crate::offer::error::OfferStoreError;
2use async_trait::async_trait;
3use sha2::Digest;
4use std::collections::HashMap;
5use std::sync::Arc;
6use switchgear_service_api::lnurl::LnUrlOfferMetadata;
7use switchgear_service_api::offer::{
8    Offer, OfferMetadata, OfferMetadataStore, OfferProvider, OfferRecord, OfferStore,
9};
10use switchgear_service_api::service::ServiceErrorSource;
11use tokio::sync::Mutex;
12use uuid::Uuid;
13
14#[derive(Clone, Debug)]
15struct OfferRecordTimestamped {
16    created: chrono::DateTime<chrono::Utc>,
17    offer: OfferRecord,
18}
19
20#[derive(Clone, Debug)]
21struct OfferMetadataTimestamped {
22    created: chrono::DateTime<chrono::Utc>,
23    metadata: OfferMetadata,
24}
25
26#[derive(Clone, Debug)]
27pub struct MemoryOfferStore {
28    offer: Arc<Mutex<HashMap<(String, Uuid), OfferRecordTimestamped>>>,
29    metadata: Arc<Mutex<HashMap<(String, Uuid), OfferMetadataTimestamped>>>,
30}
31
32impl MemoryOfferStore {
33    pub fn new() -> Self {
34        Self {
35            offer: Arc::new(Mutex::new(HashMap::new())),
36            metadata: Arc::new(Mutex::new(HashMap::new())),
37        }
38    }
39}
40
41impl Default for MemoryOfferStore {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47#[async_trait]
48impl OfferStore for MemoryOfferStore {
49    type Error = OfferStoreError;
50
51    async fn get_offer(
52        &self,
53        partition: &str,
54        id: &Uuid,
55    ) -> Result<Option<OfferRecord>, Self::Error> {
56        let store = self.offer.lock().await;
57        Ok(store
58            .get(&(partition.to_string(), *id))
59            .map(|o| o.offer.clone()))
60    }
61
62    async fn get_offers(
63        &self,
64        partition: &str,
65        start: usize,
66        count: usize,
67    ) -> Result<Vec<OfferRecord>, Self::Error> {
68        let store = self.offer.lock().await;
69        let mut offers: Vec<OfferRecordTimestamped> = store
70            .iter()
71            .filter(|((p, _), _)| p == partition)
72            .map(|(_, offer)| offer.clone())
73            .collect();
74
75        offers.sort_by(|a, b| {
76            a.created
77                .cmp(&b.created)
78                .then_with(|| a.offer.id.cmp(&b.offer.id))
79        });
80
81        let offers = offers
82            .into_iter()
83            .skip(start)
84            .take(count)
85            .map(|o| o.offer)
86            .collect();
87
88        Ok(offers)
89    }
90
91    async fn post_offer(&self, offer: OfferRecord) -> Result<Option<Uuid>, Self::Error> {
92        let metadata_store = self.metadata.lock().await;
93        let mut store = self.offer.lock().await;
94
95        if !metadata_store.contains_key(&(offer.partition.to_string(), offer.offer.metadata_id)) {
96            return Err(OfferStoreError::invalid_input_error(
97                format!("post offer {offer:?}"),
98                format!(
99                    "metadata {} not found for offer {}",
100                    offer.offer.metadata_id, offer.id
101                ),
102            ));
103        }
104
105        if let std::collections::hash_map::Entry::Vacant(e) =
106            store.entry((offer.partition.to_string(), offer.id))
107        {
108            e.insert(OfferRecordTimestamped {
109                created: chrono::Utc::now(),
110                offer: offer.clone(),
111            });
112            Ok(Some(offer.id))
113        } else {
114            Ok(None)
115        }
116    }
117
118    async fn put_offer(&self, offer: OfferRecord) -> Result<bool, Self::Error> {
119        let metadata_store = self.metadata.lock().await;
120        let mut store = self.offer.lock().await;
121
122        if !metadata_store.contains_key(&(offer.partition.to_string(), offer.offer.metadata_id)) {
123            return Err(OfferStoreError::invalid_input_error(
124                format!("put offer {offer:?}"),
125                format!(
126                    "metadata {} not found for offer {}",
127                    offer.offer.metadata_id, offer.id
128                ),
129            ));
130        }
131
132        let was_new = store
133            .insert(
134                (offer.partition.to_string(), offer.id),
135                OfferRecordTimestamped {
136                    created: chrono::Utc::now(),
137                    offer,
138                },
139            )
140            .is_none();
141        Ok(was_new)
142    }
143
144    async fn delete_offer(&self, partition: &str, id: &Uuid) -> Result<bool, Self::Error> {
145        let mut store = self.offer.lock().await;
146        Ok(store.remove(&(partition.to_string(), *id)).is_some())
147    }
148}
149
150#[async_trait]
151impl OfferProvider for MemoryOfferStore {
152    type Error = OfferStoreError;
153
154    async fn offer(
155        &self,
156        _hostname: &str,
157        partition: &str,
158        id: &Uuid,
159    ) -> Result<Option<Offer>, Self::Error> {
160        if let Some(offer) = self.get_offer(partition, id).await? {
161            let offer_metadata = match self
162                .get_metadata(partition, &offer.offer.metadata_id)
163                .await?
164            {
165                Some(metadata) => metadata,
166                None => {
167                    return Ok(None);
168                }
169            };
170
171            let lnurl_metadata = LnUrlOfferMetadata(offer_metadata.metadata);
172            let metadata_json_string = serde_json::to_string(&lnurl_metadata).map_err(|e| {
173                OfferStoreError::serialization_error(
174                    ServiceErrorSource::Internal,
175                    format!(
176                        "serializing LnUrlOfferMetadata while building LNURL offer response for {offer:?}"
177                    ),
178                    e,
179                )
180            })?;
181
182            let metadata_json_hash = sha2::Sha256::digest(metadata_json_string.as_bytes())
183                .to_vec()
184                .try_into()
185                .map_err(|_| {
186                    OfferStoreError::hash_conversion_error(
187                        ServiceErrorSource::Internal,
188                        format!(
189                            "hashing LnUrlOfferMetadata json string {metadata_json_string} while building LNURL offer response for {offer:?}"
190                        ),
191                    )
192                })?;
193
194            Ok(Some(Offer {
195                partition: offer.partition,
196                id: offer.id,
197                max_sendable: offer.offer.max_sendable,
198                min_sendable: offer.offer.min_sendable,
199                metadata_json_string,
200                metadata_json_hash,
201                timestamp: offer.offer.timestamp,
202                expires: offer.offer.expires,
203            }))
204        } else {
205            Ok(None)
206        }
207    }
208}
209
210#[async_trait]
211impl OfferMetadataStore for MemoryOfferStore {
212    type Error = OfferStoreError;
213
214    async fn get_metadata(
215        &self,
216        partition: &str,
217        id: &Uuid,
218    ) -> Result<Option<OfferMetadata>, Self::Error> {
219        let store = self.metadata.lock().await;
220        Ok(store
221            .get(&(partition.to_string(), *id))
222            .map(|o| o.metadata.clone()))
223    }
224
225    async fn get_all_metadata(
226        &self,
227        partition: &str,
228        start: usize,
229        count: usize,
230    ) -> Result<Vec<OfferMetadata>, Self::Error> {
231        let store = self.metadata.lock().await;
232        let mut metadata: Vec<OfferMetadataTimestamped> = store
233            .iter()
234            .filter(|((p, _), _)| p == partition)
235            .map(|(_, metadata)| metadata.clone())
236            .collect();
237
238        metadata.sort_by(|a, b| {
239            a.created
240                .cmp(&b.created)
241                .then_with(|| a.metadata.id.cmp(&b.metadata.id))
242        });
243
244        let metadata = metadata
245            .into_iter()
246            .skip(start)
247            .take(count)
248            .map(|o| o.metadata)
249            .collect();
250
251        Ok(metadata)
252    }
253
254    async fn post_metadata(&self, metadata: OfferMetadata) -> Result<Option<Uuid>, Self::Error> {
255        let mut store = self.metadata.lock().await;
256        if let std::collections::hash_map::Entry::Vacant(e) =
257            store.entry((metadata.partition.to_string(), metadata.id))
258        {
259            e.insert(OfferMetadataTimestamped {
260                created: chrono::Utc::now(),
261                metadata: metadata.clone(),
262            });
263
264            Ok(Some(metadata.id))
265        } else {
266            Ok(None)
267        }
268    }
269
270    async fn put_metadata(&self, metadata: OfferMetadata) -> Result<bool, Self::Error> {
271        let mut store = self.metadata.lock().await;
272        let was_new = store
273            .insert(
274                (metadata.partition.to_string(), metadata.id),
275                OfferMetadataTimestamped {
276                    created: chrono::Utc::now(),
277                    metadata,
278                },
279            )
280            .is_none();
281        Ok(was_new)
282    }
283
284    async fn delete_metadata(&self, partition: &str, id: &Uuid) -> Result<bool, Self::Error> {
285        let offer_store = self.offer.lock().await;
286        let mut metadata_store = self.metadata.lock().await;
287
288        let metadata_in_use = offer_store.values().any(|offer| {
289            offer.offer.partition == partition && offer.offer.offer.metadata_id == *id
290        });
291
292        if metadata_in_use {
293            return Err(OfferStoreError::invalid_input_error(
294                format!("delete metadata {partition}/{id}"),
295                format!("metadata {} is referenced by existing offers", id),
296            ));
297        }
298
299        Ok(metadata_store
300            .remove(&(partition.to_string(), *id))
301            .is_some())
302    }
303}