switchgear_components/offer/
memory.rs1use 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}