1use std::collections::HashMap;
2use std::sync::Arc;
3
4use anyhow::anyhow;
5use fedimint_api_client::api::net::Connector;
6use fedimint_client::{Client, ClientHandleArc, ClientModuleInstance};
7use fedimint_core::config::FederationId;
8use fedimint_core::core::{ModuleKind, OperationId};
9use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped, IRawDatabase};
10use fedimint_core::encoding::{Decodable, Encodable};
11use fedimint_core::invite_code::InviteCode;
12use fedimint_core::secp256k1::hashes::sha256;
13use fedimint_core::util::SafeUrl;
14use fedimint_core::{Amount, BitcoinHash};
15use fedimint_derive_secret::DerivableSecret;
16use fedimint_ln_client::recurring::{
17 PaymentCodeId, PaymentCodeRootKey, RecurringPaymentError, RecurringPaymentProtocol,
18};
19use fedimint_ln_client::{LightningClientInit, LightningClientModule, LnReceiveState};
20use fedimint_mint_client::MintClientInit;
21use futures::StreamExt;
22use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Sha256};
23use lnurl::Tag;
24use lnurl::lnurl::LnUrl;
25use lnurl::pay::{LnURLPayInvoice, PayResponse};
26use tokio::sync::{Notify, RwLock};
27use tracing::{info, warn};
28
29use crate::db::{
30 FederationDbPrefix, PaymentCodeEntry, PaymentCodeInvoiceEntry, PaymentCodeInvoiceKey,
31 PaymentCodeKey, PaymentCodeNextInvoiceIndexKey, PaymentCodeVariant,
32 load_federation_client_databases, open_client_db, try_add_federation_database,
33};
34
35mod db;
36
37#[derive(Clone)]
38pub struct RecurringInvoiceServer {
39 db: Database,
40 clients: Arc<RwLock<HashMap<FederationId, ClientHandleArc>>>,
41 invoice_generated: Arc<Notify>,
42 base_url: SafeUrl,
43}
44
45impl RecurringInvoiceServer {
46 pub async fn new(db: impl IRawDatabase + 'static, base_url: SafeUrl) -> anyhow::Result<Self> {
47 let db = Database::new(db, Default::default());
48
49 let mut clients = HashMap::<_, ClientHandleArc>::new();
50
51 for (federation_id, db) in load_federation_client_databases(&db).await {
52 let mut client_builder = Client::builder(db).await?;
53 client_builder.with_module(LightningClientInit::default());
54 client_builder.with_module(MintClientInit);
55 client_builder.with_primary_module_kind(ModuleKind::from_static_str("mint"));
56 let client = client_builder.open(Self::default_secret()).await?;
57 clients.insert(federation_id, Arc::new(client));
58 }
59
60 Ok(Self {
61 db,
62 clients: Arc::new(RwLock::new(clients)),
63 invoice_generated: Arc::new(Default::default()),
64 base_url,
65 })
66 }
67
68 fn default_secret() -> DerivableSecret {
72 DerivableSecret::new_root(&[], &[])
73 }
74
75 pub async fn register_federation(
76 &self,
77 invite_code: &InviteCode,
78 ) -> Result<FederationId, RecurringPaymentError> {
79 let federation_id = invite_code.federation_id();
80 info!("Registering federation {}", federation_id);
81
82 let mut clients = self.clients.write().await;
85 if clients.contains_key(&federation_id) {
86 return Err(RecurringPaymentError::FederationAlreadyRegistered(
87 federation_id,
88 ));
89 }
90
91 let client_db_prefix = FederationDbPrefix::random();
96 let client_db = open_client_db(&self.db, client_db_prefix);
97
98 match Self::join_federation_static(client_db, invite_code).await {
99 Ok(client) => {
100 try_add_federation_database(&self.db, federation_id, client_db_prefix)
101 .await
102 .expect("We hold a global lock, no parallel joining can happen");
103 clients.insert(federation_id, client);
104 Ok(federation_id)
105 }
106 Err(e) => {
107 Err(e)
109 }
110 }
111 }
112
113 async fn join_federation_static(
114 client_db: Database,
115 invite_code: &InviteCode,
116 ) -> Result<ClientHandleArc, RecurringPaymentError> {
117 let config = Connector::default()
118 .download_from_invite_code(invite_code)
119 .await
120 .map_err(RecurringPaymentError::JoiningFederationFailed)?;
121
122 let mut client_builder = Client::builder(client_db)
123 .await
124 .map_err(RecurringPaymentError::JoiningFederationFailed)?;
125
126 client_builder.with_connector(Connector::default());
127 client_builder.with_module(LightningClientInit::default());
128 client_builder.with_module(MintClientInit);
129 client_builder.with_primary_module_kind(ModuleKind::from_static_str("mint"));
130
131 let client = client_builder
132 .join(Self::default_secret(), config, None)
133 .await
134 .map_err(RecurringPaymentError::JoiningFederationFailed)?;
135 Ok(Arc::new(client))
136 }
137
138 pub async fn register_recurring_payment_code(
139 &self,
140 federation_id: FederationId,
141 payment_code_root_key: PaymentCodeRootKey,
142 protocol: RecurringPaymentProtocol,
143 meta: &str,
144 ) -> Result<String, RecurringPaymentError> {
145 if protocol != RecurringPaymentProtocol::LNURL {
147 return Err(RecurringPaymentError::UnsupportedProtocol(protocol));
148 }
149
150 self.get_federation_client(federation_id).await?;
152
153 let payment_code = self.create_lnurl(payment_code_root_key.to_payment_code_id());
154 let payment_code_entry = PaymentCodeEntry {
155 root_key: payment_code_root_key,
156 federation_id,
157 protocol,
158 payment_code: payment_code.clone(),
159 variant: PaymentCodeVariant::Lnurl {
160 meta: meta.to_owned(),
161 },
162 };
163
164 let mut dbtx = self.db.begin_transaction().await;
165 if let Some(existing_code) = dbtx
166 .insert_entry(
167 &PaymentCodeKey {
168 payment_code_id: payment_code_root_key.to_payment_code_id(),
169 },
170 &payment_code_entry,
171 )
172 .await
173 {
174 if existing_code != payment_code_entry {
175 return Err(RecurringPaymentError::PaymentCodeAlreadyExists(
176 payment_code_root_key,
177 ));
178 }
179
180 dbtx.ignore_uncommitted();
181 return Ok(payment_code);
182 }
183
184 dbtx.insert_new_entry(
185 &PaymentCodeNextInvoiceIndexKey {
186 payment_code_id: payment_code_root_key.to_payment_code_id(),
187 },
188 &0,
189 )
190 .await;
191 dbtx.commit_tx_result().await?;
192
193 Ok(payment_code)
194 }
195
196 fn create_lnurl(&self, payment_code_id: PaymentCodeId) -> String {
197 let lnurl = LnUrl::from_url(format!(
198 "{}lnv1/paycodes/{}",
199 self.base_url, payment_code_id
200 ));
201 lnurl.encode()
202 }
203
204 pub async fn lnurl_pay(
205 &self,
206 payment_code_id: PaymentCodeId,
207 ) -> Result<PayResponse, RecurringPaymentError> {
208 let payment_code = self.get_payment_code(payment_code_id).await?;
209 let PaymentCodeVariant::Lnurl { meta } = payment_code.variant;
210
211 Ok(PayResponse {
212 callback: format!("{}lnv1/paycodes/{}/invoice", self.base_url, payment_code_id),
213 max_sendable: 100000000000,
214 min_sendable: 1,
215 tag: Tag::PayRequest,
216 metadata: meta,
217 comment_allowed: None,
218 allows_nostr: None,
219 nostr_pubkey: None,
220 })
221 }
222
223 pub async fn lnurl_invoice(
224 &self,
225 payment_code_id: PaymentCodeId,
226 amount: Amount,
227 ) -> Result<LnURLPayInvoice, RecurringPaymentError> {
228 Ok(LnURLPayInvoice::new(
229 self.create_bolt11_invoice(payment_code_id, amount)
230 .await?
231 .to_string(),
232 ))
233 }
234
235 async fn create_bolt11_invoice(
236 &self,
237 payment_code_id: PaymentCodeId,
238 amount: Amount,
239 ) -> Result<Bolt11Invoice, RecurringPaymentError> {
240 const DEFAULT_EXPIRY_TIME: u64 = 60 * 60 * 24;
243
244 let payment_code = self.get_payment_code(payment_code_id).await?;
245 let invoice_index = self.get_next_invoice_index(payment_code_id).await;
246
247 let federation_client = self
248 .get_federation_client(payment_code.federation_id)
249 .await?;
250 let federation_client_ln_module = federation_client
251 .get_first_module::<LightningClientModule>()
252 .map_err(|e| {
253 warn!("No compatible lightning module found {e}");
254 RecurringPaymentError::NoLightningModuleFound
255 })?;
256
257 let gateway = federation_client_ln_module
258 .get_gateway(None, false)
259 .await?
260 .ok_or(RecurringPaymentError::NoGatewayFound)?;
261
262 let lnurl_meta = match payment_code.variant {
263 PaymentCodeVariant::Lnurl { meta } => meta,
264 };
265 let meta_hash = Sha256(sha256::Hash::hash(lnurl_meta.as_bytes()));
266 let description = Bolt11InvoiceDescription::Hash(&meta_hash);
267
268 let (operation_id, invoice, _preimage) = federation_client_ln_module
271 .create_bolt11_invoice_for_user_tweaked(
272 amount,
273 description,
274 Some(DEFAULT_EXPIRY_TIME),
275 payment_code.root_key.0,
276 invoice_index,
277 serde_json::Value::Null,
278 Some(gateway),
279 )
280 .await?;
281
282 let mut dbtx = self.db.begin_transaction().await;
283 dbtx.insert_new_entry(
284 &PaymentCodeInvoiceKey {
285 payment_code_id,
286 index: invoice_index,
287 },
288 &PaymentCodeInvoiceEntry {
289 operation_id,
290 invoice: PaymentCodeInvoice::Bolt11(invoice.clone()),
291 },
292 )
293 .await;
294
295 let invoice_generated_notifier = self.invoice_generated.clone();
296 dbtx.on_commit(move || {
297 invoice_generated_notifier.notify_waiters();
298 });
299 dbtx.commit_tx().await;
300
301 await_invoice_confirmed(&federation_client_ln_module, operation_id).await?;
302
303 Ok(invoice)
304 }
305
306 async fn get_federation_client(
307 &self,
308 federation_id: FederationId,
309 ) -> Result<ClientHandleArc, RecurringPaymentError> {
310 self.clients
311 .read()
312 .await
313 .get(&federation_id)
314 .cloned()
315 .ok_or(RecurringPaymentError::UnknownFederationId(federation_id))
316 }
317
318 pub async fn await_invoice_index_generated(
319 &self,
320 payment_code_id: PaymentCodeId,
321 invoice_index: u64,
322 ) -> Result<PaymentCodeInvoiceEntry, RecurringPaymentError> {
323 self.get_payment_code(payment_code_id).await?;
324
325 let mut notified = self.invoice_generated.notified();
326 loop {
327 let mut dbtx = self.db.begin_transaction_nc().await;
328 if let Some(invoice_entry) = dbtx
329 .get_value(&PaymentCodeInvoiceKey {
330 payment_code_id,
331 index: invoice_index,
332 })
333 .await
334 {
335 break Ok(invoice_entry);
336 };
337
338 notified.await;
339 notified = self.invoice_generated.notified();
340 }
341 }
342
343 async fn get_next_invoice_index(&self, payment_code_id: PaymentCodeId) -> u64 {
344 self.db
345 .autocommit(
346 |dbtx, _| {
347 Box::pin(async move {
348 let next_index = dbtx
349 .get_value(&PaymentCodeNextInvoiceIndexKey { payment_code_id })
350 .await
351 .map(|index| index + 1)
352 .unwrap_or(0);
353 dbtx.insert_entry(
354 &PaymentCodeNextInvoiceIndexKey { payment_code_id },
355 &next_index,
356 )
357 .await;
358 Result::<_, ()>::Ok(next_index)
359 })
360 },
361 None,
362 )
363 .await
364 .expect("Loops forever and never returns errors internally")
365 }
366
367 pub async fn list_federations(&self) -> Vec<FederationId> {
368 self.clients.read().await.keys().cloned().collect()
369 }
370
371 async fn get_payment_code(
372 &self,
373 payment_code_id: PaymentCodeId,
374 ) -> Result<PaymentCodeEntry, RecurringPaymentError> {
375 self.db
376 .begin_transaction_nc()
377 .await
378 .get_value(&PaymentCodeKey { payment_code_id })
379 .await
380 .ok_or(RecurringPaymentError::UnknownPaymentCode(payment_code_id))
381 }
382}
383
384async fn await_invoice_confirmed(
385 ln_module: &ClientModuleInstance<'_, LightningClientModule>,
386 operation_id: OperationId,
387) -> Result<(), RecurringPaymentError> {
388 let mut operation_updated = ln_module
389 .subscribe_ln_receive(operation_id)
390 .await?
391 .into_stream();
392
393 while let Some(update) = operation_updated.next().await {
394 if matches!(update, LnReceiveState::WaitingForPayment { .. }) {
395 return Ok(());
396 }
397 }
398
399 Err(RecurringPaymentError::Other(anyhow!(
400 "BOLT11 invoice not confirmed"
401 )))
402}
403
404#[derive(Debug, Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
405pub enum PaymentCodeInvoice {
406 Bolt11(Bolt11Invoice),
407}