breez_sdk_spark/sdk/
lightning_address.rs1use lnurl_models::sanitize_username;
2
3use crate::{
4 CheckLightningAddressRequest, LightningAddressInfo, LnurlInfo, RegisterLightningAddressRequest,
5 error::SdkError, persist::ObjectCacheRepository,
6};
7
8use super::BreezSdk;
9
10#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
11#[allow(clippy::needless_pass_by_value)]
12impl BreezSdk {
13 pub async fn check_lightning_address_available(
14 &self,
15 req: CheckLightningAddressRequest,
16 ) -> Result<bool, SdkError> {
17 let Some(client) = &self.lnurl_server_client else {
18 return Err(SdkError::Generic(
19 "LNURL server is not configured".to_string(),
20 ));
21 };
22
23 let username = sanitize_username(&req.username);
24 let available = client.check_username_available(&username).await?;
25 Ok(available)
26 }
27
28 pub async fn get_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
29 let cache = ObjectCacheRepository::new(self.storage.clone());
30 let cached = cache.fetch_lightning_address().await?;
31 if cached.is_none() && self.lnurl_server_client.is_some() {
32 return self.recover_lightning_address().await;
33 }
34 Ok(cached.flatten())
35 }
36
37 pub async fn register_lightning_address(
38 &self,
39 request: RegisterLightningAddressRequest,
40 ) -> Result<LightningAddressInfo, SdkError> {
41 self.ensure_spark_private_mode_initialized().await?;
43
44 self.register_lightning_address_internal(request).await
45 }
46
47 pub async fn delete_lightning_address(&self) -> Result<(), SdkError> {
48 let cache = ObjectCacheRepository::new(self.storage.clone());
49 let Some(address_info) = cache.fetch_lightning_address().await?.flatten() else {
50 return Ok(());
51 };
52
53 let Some(client) = &self.lnurl_server_client else {
54 return Err(SdkError::Generic(
55 "LNURL server is not configured".to_string(),
56 ));
57 };
58
59 let params = crate::lnurl::UnregisterLightningAddressRequest {
60 username: address_info.username,
61 };
62
63 client.unregister_lightning_address(¶ms).await?;
64 cache.delete_lightning_address().await?;
65 Ok(())
66 }
67}
68
69impl BreezSdk {
71 pub(super) async fn recover_lightning_address(
73 &self,
74 ) -> Result<Option<LightningAddressInfo>, SdkError> {
75 let cache = ObjectCacheRepository::new(self.storage.clone());
76
77 let Some(client) = &self.lnurl_server_client else {
78 return Err(SdkError::Generic(
79 "LNURL server is not configured".to_string(),
80 ));
81 };
82 let resp = client.recover_lightning_address().await?;
83
84 let result = if let Some(resp) = resp {
85 let address_info = resp.into();
86 cache.save_lightning_address(&address_info).await?;
87 Some(address_info)
88 } else {
89 cache.delete_lightning_address().await?;
90 None
91 };
92
93 Ok(result)
94 }
95
96 pub(super) async fn register_lightning_address_internal(
97 &self,
98 request: RegisterLightningAddressRequest,
99 ) -> Result<LightningAddressInfo, SdkError> {
100 let cache = ObjectCacheRepository::new(self.storage.clone());
101 let Some(client) = &self.lnurl_server_client else {
102 return Err(SdkError::Generic(
103 "LNURL server is not configured".to_string(),
104 ));
105 };
106
107 let username = sanitize_username(&request.username);
108
109 let description = match request.description {
110 Some(description) => description,
111 None => format!("Pay to {}@{}", username, client.domain()),
112 };
113
114 let params = crate::lnurl::RegisterLightningAddressRequest {
115 username: username.clone(),
116 description: description.clone(),
117 lnurl_private_mode_enabled: !self.config.support_lnurl_verify,
118 };
119
120 let response = client.register_lightning_address(¶ms).await?;
121 let address_info = LightningAddressInfo {
122 lightning_address: response.lightning_address,
123 description,
124 lnurl: LnurlInfo::new(response.lnurl),
125 username,
126 };
127 cache.save_lightning_address(&address_info).await?;
128 Ok(address_info)
129 }
130}
131
132#[cfg(test)]
133#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
134mod tests {
135 use std::{path::PathBuf, sync::Arc};
136
137 use crate::{
138 LightningAddressInfo, LnurlInfo, persist::Storage, persist::sqlite::SqliteStorage,
139 };
140
141 use crate::persist::ObjectCacheRepository;
142
143 fn create_temp_dir(name: &str) -> PathBuf {
144 let mut path = std::env::temp_dir();
145 path.push(format!("breez-test-{}-{}", name, uuid::Uuid::new_v4()));
146 std::fs::create_dir_all(&path).unwrap();
147 path
148 }
149
150 fn create_temp_storage(name: &str) -> (Arc<SqliteStorage>, PathBuf) {
151 let dir = create_temp_dir(name);
152 let storage = SqliteStorage::new(&dir).expect("Failed to create storage");
153 (Arc::new(storage), dir)
154 }
155
156 fn sample_address_info() -> LightningAddressInfo {
157 LightningAddressInfo {
158 lightning_address: "test@example.com".to_string(),
159 username: "test".to_string(),
160 description: "Test address".to_string(),
161 lnurl: LnurlInfo::new("https://example.com/.well-known/lnurlp/test".to_string()),
162 }
163 }
164
165 #[tokio::test]
166 async fn test_fetch_returns_none_when_never_recovered() {
167 let (storage, _dir) = create_temp_storage("never_recovered");
168 let cache = ObjectCacheRepository::new(storage as Arc<_>);
169
170 let result = cache.fetch_lightning_address().await.unwrap();
172 assert!(result.is_none());
173 }
174
175 #[tokio::test]
176 async fn test_fetch_returns_some_none_after_delete() {
177 let (storage, _dir) = create_temp_storage("after_delete");
178 let cache = ObjectCacheRepository::new(storage as Arc<_>);
179
180 cache
182 .save_lightning_address(&sample_address_info())
183 .await
184 .unwrap();
185 cache.delete_lightning_address().await.unwrap();
186
187 let result = cache.fetch_lightning_address().await.unwrap();
189 assert!(
190 matches!(result, Some(None)),
191 "Expected Some(None) after delete"
192 );
193 }
194
195 #[tokio::test]
196 async fn test_fetch_returns_some_some_after_save() {
197 let (storage, _dir) = create_temp_storage("after_save");
198 let cache = ObjectCacheRepository::new(storage as Arc<_>);
199
200 cache
201 .save_lightning_address(&sample_address_info())
202 .await
203 .unwrap();
204
205 let result = cache.fetch_lightning_address().await.unwrap();
207 let info = result
208 .flatten()
209 .expect("Expected Some(Some(info)) after save");
210 assert_eq!(info.lightning_address, "test@example.com");
211 }
212
213 #[tokio::test]
214 async fn test_backward_compat_old_cached_json() {
215 let (storage, _dir) = create_temp_storage("backward_compat");
216
217 let old_value = serde_json::to_string(&sample_address_info()).unwrap();
219 storage
220 .set_cached_item("lightning_address".to_string(), old_value)
221 .await
222 .unwrap();
223
224 let cache = ObjectCacheRepository::new(storage as Arc<_>);
225 let result = cache.fetch_lightning_address().await.unwrap();
226 let info = result
227 .flatten()
228 .expect("Expected old cached JSON to deserialize as Some(info)");
229 assert_eq!(info.lightning_address, "test@example.com");
230 }
231}