1use bitcoin::hex::DisplayHex;
2use lnurl_models::sanitize_username;
3
4use crate::{
5 AuthorizeTransferRequest, CheckLightningAddressRequest, ClaimTransferRequest,
6 LightningAddressInfo, LnurlInfo, RegisterLightningAddressRequest, TransferAuthorization,
7 error::SdkError, lnurl::LnurlServerError, persist::ObjectCacheRepository,
8};
9
10use super::BreezSdk;
11
12#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
13#[allow(clippy::needless_pass_by_value)]
14impl BreezSdk {
15 pub async fn check_lightning_address_available(
16 &self,
17 req: CheckLightningAddressRequest,
18 ) -> Result<bool, SdkError> {
19 let Some(client) = &self.lnurl_server_client else {
20 return Err(SdkError::Generic(
21 "LNURL server is not configured".to_string(),
22 ));
23 };
24
25 let username = sanitize_username(&req.username);
26 let available = client.check_username_available(&username).await?;
27 Ok(available)
28 }
29
30 pub async fn get_lightning_address(&self) -> Result<Option<LightningAddressInfo>, SdkError> {
31 let cache = ObjectCacheRepository::new(self.storage.clone());
32 let cached = cache.fetch_lightning_address().await?;
33 if cached.is_none() && self.lnurl_server_client.is_some() {
34 return self.recover_lightning_address().await;
35 }
36 Ok(cached.flatten())
37 }
38
39 pub async fn register_lightning_address(
40 &self,
41 request: RegisterLightningAddressRequest,
42 ) -> Result<LightningAddressInfo, SdkError> {
43 let cache = ObjectCacheRepository::new(self.storage.clone());
44 let Some(client) = &self.lnurl_server_client else {
45 return Err(SdkError::Generic(
46 "LNURL server is not configured".to_string(),
47 ));
48 };
49
50 let username = sanitize_username(&request.username);
51
52 let description = match request.description {
53 Some(description) => description,
54 None => format!("Pay to {}@{}", username, client.domain()),
55 };
56
57 let params = crate::lnurl::RegisterLightningAddressRequest {
58 username: username.clone(),
59 description: description.clone(),
60 };
61
62 let response = client.register_lightning_address(¶ms).await?;
63 let address_info = LightningAddressInfo {
64 lightning_address: response.lightning_address,
65 description,
66 lnurl: LnurlInfo::new(response.lnurl),
67 username,
68 };
69 cache.save_lightning_address(&address_info, false).await?;
70 Ok(address_info)
71 }
72
73 pub async fn authorize_lightning_address_transfer(
79 &self,
80 request: AuthorizeTransferRequest,
81 ) -> Result<TransferAuthorization, SdkError> {
82 let cache = ObjectCacheRepository::new(self.storage.clone());
83 let Some(address_info) = cache.fetch_lightning_address().await?.flatten() else {
84 return Err(SdkError::Generic(
85 "No lightning address registered to transfer".to_string(),
86 ));
87 };
88 let self_pubkey = self.spark_wallet.get_identity_public_key().to_string();
89 let message = format!(
90 "transfer:{}-{}",
91 address_info.username, request.transferee_pubkey
92 );
93 let signature = self.spark_wallet.sign_message(&message).await?;
94 Ok(TransferAuthorization {
95 username: address_info.username,
96 pubkey: self_pubkey,
97 signature: signature.serialize_der().to_lower_hex_string(),
98 })
99 }
100
101 pub async fn claim_lightning_address_transfer(
106 &self,
107 request: ClaimTransferRequest,
108 ) -> Result<LightningAddressInfo, SdkError> {
109 let cache = ObjectCacheRepository::new(self.storage.clone());
110 let Some(client) = &self.lnurl_server_client else {
111 return Err(SdkError::Generic(
112 "LNURL server is not configured".to_string(),
113 ));
114 };
115
116 let username = sanitize_username(&request.authorization.username);
117 let description = match request.description {
118 Some(description) => description,
119 None => format!("Pay to {}@{}", username, client.domain()),
120 };
121
122 let params = crate::lnurl::TransferLightningAddressRequest {
123 username: username.clone(),
124 description: description.clone(),
125 from_pubkey: request.authorization.pubkey,
126 from_signature: request.authorization.signature,
127 };
128
129 let response = client.transfer_lightning_address(¶ms).await?;
130 let address_info = LightningAddressInfo {
131 lightning_address: response.lightning_address,
132 description,
133 lnurl: LnurlInfo::new(response.lnurl),
134 username,
135 };
136 cache.save_lightning_address(&address_info, false).await?;
137 Ok(address_info)
138 }
139
140 pub async fn delete_lightning_address(&self) -> Result<(), SdkError> {
141 let cache = ObjectCacheRepository::new(self.storage.clone());
142 let Some(address_info) = cache.fetch_lightning_address().await?.flatten() else {
143 return Ok(());
144 };
145
146 let Some(client) = &self.lnurl_server_client else {
147 return Err(SdkError::Generic(
148 "LNURL server is not configured".to_string(),
149 ));
150 };
151
152 let params = crate::lnurl::UnregisterLightningAddressRequest {
153 username: address_info.username,
154 };
155
156 match client.unregister_lightning_address(¶ms).await {
157 Ok(()) => {}
158 Err(
164 e @ LnurlServerError::Network {
165 statuscode: 409, ..
166 },
167 ) => {
168 self.recover_lightning_address().await?;
169 return Err(e.into());
170 }
171 Err(e) => return Err(e.into()),
172 }
173
174 cache.delete_lightning_address(false).await?;
175 Ok(())
176 }
177}
178
179impl BreezSdk {
181 pub(super) async fn recover_lightning_address(
183 &self,
184 ) -> Result<Option<LightningAddressInfo>, SdkError> {
185 let cache = ObjectCacheRepository::new(self.storage.clone());
186
187 let Some(client) = &self.lnurl_server_client else {
188 return Err(SdkError::Generic(
189 "LNURL server is not configured".to_string(),
190 ));
191 };
192 let resp = client.recover_lightning_address().await?;
193
194 let result = if let Some(resp) = resp {
195 let address_info = resp.into();
196 cache.save_lightning_address(&address_info, true).await?;
197 Some(address_info)
198 } else {
199 cache.delete_lightning_address(true).await?;
200 None
201 };
202
203 Ok(result)
204 }
205}
206
207#[cfg(test)]
208#[cfg(feature = "sqlite")]
209mod tests {
210 use std::{path::PathBuf, sync::Arc};
211
212 use crate::{LightningAddressInfo, LnurlInfo, persist::sqlite::SqliteStorage};
213
214 use crate::persist::ObjectCacheRepository;
215
216 fn create_temp_dir(name: &str) -> PathBuf {
217 let mut path = std::env::temp_dir();
218 path.push(format!("breez-test-{}-{}", name, uuid::Uuid::new_v4()));
219 std::fs::create_dir_all(&path).unwrap();
220 path
221 }
222
223 fn create_temp_storage(name: &str) -> (Arc<SqliteStorage>, PathBuf) {
224 let dir = create_temp_dir(name);
225 let storage = SqliteStorage::new(&dir).expect("Failed to create storage");
226 (Arc::new(storage), dir)
227 }
228
229 fn sample_address_info() -> LightningAddressInfo {
230 LightningAddressInfo {
231 lightning_address: "test@example.com".to_string(),
232 username: "test".to_string(),
233 description: "Test address".to_string(),
234 lnurl: LnurlInfo::new("https://example.com/.well-known/lnurlp/test".to_string()),
235 }
236 }
237
238 #[tokio::test]
239 async fn test_fetch_returns_none_when_never_recovered() {
240 let (storage, _dir) = create_temp_storage("never_recovered");
241 let cache = ObjectCacheRepository::new(storage as Arc<_>);
242
243 let result = cache.fetch_lightning_address().await.unwrap();
245 assert!(result.is_none());
246 }
247
248 #[tokio::test]
249 async fn test_fetch_returns_some_none_after_delete() {
250 let (storage, _dir) = create_temp_storage("after_delete");
251 let cache = ObjectCacheRepository::new(storage as Arc<_>);
252
253 cache
255 .save_lightning_address(&sample_address_info(), false)
256 .await
257 .unwrap();
258 cache.delete_lightning_address(false).await.unwrap();
259
260 let result = cache.fetch_lightning_address().await.unwrap();
262 assert!(
263 matches!(result, Some(None)),
264 "Expected Some(None) after delete"
265 );
266 }
267
268 #[tokio::test]
269 async fn test_fetch_returns_some_some_after_save() {
270 let (storage, _dir) = create_temp_storage("after_save");
271 let cache = ObjectCacheRepository::new(storage as Arc<_>);
272
273 cache
274 .save_lightning_address(&sample_address_info(), false)
275 .await
276 .unwrap();
277
278 let result = cache.fetch_lightning_address().await.unwrap();
280 let info = result
281 .flatten()
282 .expect("Expected Some(Some(info)) after save");
283 assert_eq!(info.lightning_address, "test@example.com");
284 }
285}