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 let cache = ObjectCacheRepository::new(self.storage.clone());
42 let Some(client) = &self.lnurl_server_client else {
43 return Err(SdkError::Generic(
44 "LNURL server is not configured".to_string(),
45 ));
46 };
47
48 let username = sanitize_username(&request.username);
49
50 let description = match request.description {
51 Some(description) => description,
52 None => format!("Pay to {}@{}", username, client.domain()),
53 };
54
55 let params = crate::lnurl::RegisterLightningAddressRequest {
56 username: username.clone(),
57 description: description.clone(),
58 };
59
60 let response = client.register_lightning_address(¶ms).await?;
61 let address_info = LightningAddressInfo {
62 lightning_address: response.lightning_address,
63 description,
64 lnurl: LnurlInfo::new(response.lnurl),
65 username,
66 };
67 cache.save_lightning_address(&address_info, false).await?;
68 Ok(address_info)
69 }
70
71 pub async fn delete_lightning_address(&self) -> Result<(), SdkError> {
72 let cache = ObjectCacheRepository::new(self.storage.clone());
73 let Some(address_info) = cache.fetch_lightning_address().await?.flatten() else {
74 return Ok(());
75 };
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
83 let params = crate::lnurl::UnregisterLightningAddressRequest {
84 username: address_info.username,
85 };
86
87 client.unregister_lightning_address(¶ms).await?;
88 cache.delete_lightning_address(false).await?;
89 Ok(())
90 }
91}
92
93impl BreezSdk {
95 pub(super) async fn recover_lightning_address(
97 &self,
98 ) -> Result<Option<LightningAddressInfo>, SdkError> {
99 let cache = ObjectCacheRepository::new(self.storage.clone());
100
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 let resp = client.recover_lightning_address().await?;
107
108 let result = if let Some(resp) = resp {
109 let address_info = resp.into();
110 cache.save_lightning_address(&address_info, true).await?;
111 Some(address_info)
112 } else {
113 cache.delete_lightning_address(true).await?;
114 None
115 };
116
117 Ok(result)
118 }
119}
120
121#[cfg(test)]
122#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
123mod tests {
124 use std::{path::PathBuf, sync::Arc};
125
126 use crate::{LightningAddressInfo, LnurlInfo, persist::sqlite::SqliteStorage};
127
128 use crate::persist::ObjectCacheRepository;
129
130 fn create_temp_dir(name: &str) -> PathBuf {
131 let mut path = std::env::temp_dir();
132 path.push(format!("breez-test-{}-{}", name, uuid::Uuid::new_v4()));
133 std::fs::create_dir_all(&path).unwrap();
134 path
135 }
136
137 fn create_temp_storage(name: &str) -> (Arc<SqliteStorage>, PathBuf) {
138 let dir = create_temp_dir(name);
139 let storage = SqliteStorage::new(&dir).expect("Failed to create storage");
140 (Arc::new(storage), dir)
141 }
142
143 fn sample_address_info() -> LightningAddressInfo {
144 LightningAddressInfo {
145 lightning_address: "test@example.com".to_string(),
146 username: "test".to_string(),
147 description: "Test address".to_string(),
148 lnurl: LnurlInfo::new("https://example.com/.well-known/lnurlp/test".to_string()),
149 }
150 }
151
152 #[tokio::test]
153 async fn test_fetch_returns_none_when_never_recovered() {
154 let (storage, _dir) = create_temp_storage("never_recovered");
155 let cache = ObjectCacheRepository::new(storage as Arc<_>);
156
157 let result = cache.fetch_lightning_address().await.unwrap();
159 assert!(result.is_none());
160 }
161
162 #[tokio::test]
163 async fn test_fetch_returns_some_none_after_delete() {
164 let (storage, _dir) = create_temp_storage("after_delete");
165 let cache = ObjectCacheRepository::new(storage as Arc<_>);
166
167 cache
169 .save_lightning_address(&sample_address_info(), false)
170 .await
171 .unwrap();
172 cache.delete_lightning_address(false).await.unwrap();
173
174 let result = cache.fetch_lightning_address().await.unwrap();
176 assert!(
177 matches!(result, Some(None)),
178 "Expected Some(None) after delete"
179 );
180 }
181
182 #[tokio::test]
183 async fn test_fetch_returns_some_some_after_save() {
184 let (storage, _dir) = create_temp_storage("after_save");
185 let cache = ObjectCacheRepository::new(storage as Arc<_>);
186
187 cache
188 .save_lightning_address(&sample_address_info(), false)
189 .await
190 .unwrap();
191
192 let result = cache.fetch_lightning_address().await.unwrap();
194 let info = result
195 .flatten()
196 .expect("Expected Some(Some(info)) after save");
197 assert_eq!(info.lightning_address, "test@example.com");
198 }
199}