switchgear_components/offer/
provider.rs1use 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 #[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 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 assert_eq!(offer.id, offer_id);
170 assert_eq!(offer.max_sendable, 5000000);
171 assert_eq!(offer.min_sendable, 1000);
172
173 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 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 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}