breez_sdk_spark/sdk/
lightning_address.rs1use bitcoin::hex::DisplayHex;
2use lnurl_models::sanitize_username;
3
4use crate::{
5 AuthorizeTransferRequest, CheckLightningAddressRequest, ClaimTransferRequest,
6 LightningAddressInfo, LnurlInfo, RegisterLightningAddressRequest, TransferAuthorization,
7 error::SdkError, 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 client.unregister_lightning_address(¶ms).await?;
157 cache.delete_lightning_address(false).await?;
158 Ok(())
159 }
160}
161
162impl BreezSdk {
164 pub(super) async fn recover_lightning_address(
166 &self,
167 ) -> Result<Option<LightningAddressInfo>, SdkError> {
168 let cache = ObjectCacheRepository::new(self.storage.clone());
169
170 let Some(client) = &self.lnurl_server_client else {
171 return Err(SdkError::Generic(
172 "LNURL server is not configured".to_string(),
173 ));
174 };
175 let resp = client.recover_lightning_address().await?;
176
177 let result = if let Some(resp) = resp {
178 let address_info = resp.into();
179 cache.save_lightning_address(&address_info, true).await?;
180 Some(address_info)
181 } else {
182 cache.delete_lightning_address(true).await?;
183 None
184 };
185
186 Ok(result)
187 }
188}
189
190#[cfg(test)]
191#[cfg(feature = "sqlite")]
192mod tests {
193 use std::{path::PathBuf, sync::Arc};
194
195 use crate::{LightningAddressInfo, LnurlInfo, persist::sqlite::SqliteStorage};
196
197 use crate::persist::ObjectCacheRepository;
198
199 fn create_temp_dir(name: &str) -> PathBuf {
200 let mut path = std::env::temp_dir();
201 path.push(format!("breez-test-{}-{}", name, uuid::Uuid::new_v4()));
202 std::fs::create_dir_all(&path).unwrap();
203 path
204 }
205
206 fn create_temp_storage(name: &str) -> (Arc<SqliteStorage>, PathBuf) {
207 let dir = create_temp_dir(name);
208 let storage = SqliteStorage::new(&dir).expect("Failed to create storage");
209 (Arc::new(storage), dir)
210 }
211
212 fn sample_address_info() -> LightningAddressInfo {
213 LightningAddressInfo {
214 lightning_address: "test@example.com".to_string(),
215 username: "test".to_string(),
216 description: "Test address".to_string(),
217 lnurl: LnurlInfo::new("https://example.com/.well-known/lnurlp/test".to_string()),
218 }
219 }
220
221 #[tokio::test]
222 async fn test_fetch_returns_none_when_never_recovered() {
223 let (storage, _dir) = create_temp_storage("never_recovered");
224 let cache = ObjectCacheRepository::new(storage as Arc<_>);
225
226 let result = cache.fetch_lightning_address().await.unwrap();
228 assert!(result.is_none());
229 }
230
231 #[tokio::test]
232 async fn test_fetch_returns_some_none_after_delete() {
233 let (storage, _dir) = create_temp_storage("after_delete");
234 let cache = ObjectCacheRepository::new(storage as Arc<_>);
235
236 cache
238 .save_lightning_address(&sample_address_info(), false)
239 .await
240 .unwrap();
241 cache.delete_lightning_address(false).await.unwrap();
242
243 let result = cache.fetch_lightning_address().await.unwrap();
245 assert!(
246 matches!(result, Some(None)),
247 "Expected Some(None) after delete"
248 );
249 }
250
251 #[tokio::test]
252 async fn test_fetch_returns_some_some_after_save() {
253 let (storage, _dir) = create_temp_storage("after_save");
254 let cache = ObjectCacheRepository::new(storage as Arc<_>);
255
256 cache
257 .save_lightning_address(&sample_address_info(), false)
258 .await
259 .unwrap();
260
261 let result = cache.fetch_lightning_address().await.unwrap();
263 let info = result
264 .flatten()
265 .expect("Expected Some(Some(info)) after save");
266 assert_eq!(info.lightning_address, "test@example.com");
267 }
268}