1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::anyhow;
6use fedimint_api_client::api::net::Connector;
7use fedimint_client::{Client, ClientHandleArc, ClientModule, ClientModuleInstance};
8use fedimint_core::config::FederationId;
9use fedimint_core::core::{ModuleKind, OperationId};
10use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped, IRawDatabase};
11use fedimint_core::encoding::{Decodable, Encodable};
12use fedimint_core::invite_code::InviteCode;
13use fedimint_core::secp256k1::hashes::sha256;
14use fedimint_core::task::timeout;
15use fedimint_core::util::SafeUrl;
16use fedimint_core::{Amount, BitcoinHash};
17use fedimint_derive_secret::DerivableSecret;
18use fedimint_ln_client::recurring::{
19 PaymentCodeId, PaymentCodeRootKey, RecurringPaymentError, RecurringPaymentProtocol,
20};
21use fedimint_ln_client::{
22 LightningClientInit, LightningClientModule, LightningOperationMeta,
23 LightningOperationMetaVariant, LnReceiveState,
24};
25use fedimint_mint_client::MintClientInit;
26use futures::StreamExt;
27use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Sha256};
28use lnurl::Tag;
29use lnurl::lnurl::LnUrl;
30use lnurl::pay::PayResponse;
31use serde::{Deserialize, Serialize};
32use tokio::sync::{Notify, RwLock};
33use tracing::{info, warn};
34
35use crate::db::{
36 FederationDbPrefix, PaymentCodeEntry, PaymentCodeInvoiceEntry, PaymentCodeInvoiceKey,
37 PaymentCodeKey, PaymentCodeNextInvoiceIndexKey, PaymentCodeVariant,
38 load_federation_client_databases, open_client_db, try_add_federation_database,
39};
40
41mod db;
42
43#[derive(Clone)]
44pub struct RecurringInvoiceServer {
45 db: Database,
46 clients: Arc<RwLock<HashMap<FederationId, ClientHandleArc>>>,
47 invoice_generated: Arc<Notify>,
48 base_url: SafeUrl,
49}
50
51impl RecurringInvoiceServer {
52 pub async fn new(db: impl IRawDatabase + 'static, base_url: SafeUrl) -> anyhow::Result<Self> {
53 let db = Database::new(db, Default::default());
54
55 let mut clients = HashMap::<_, ClientHandleArc>::new();
56
57 for (federation_id, db) in load_federation_client_databases(&db).await {
58 let mut client_builder = Client::builder(db).await?;
59 client_builder.with_module(LightningClientInit::default());
60 client_builder.with_module(MintClientInit);
61 client_builder.with_primary_module_kind(ModuleKind::from_static_str("mint"));
62 let client = client_builder
63 .open(fedimint_client::RootSecret::StandardDoubleDerive(
64 Self::default_secret(),
65 ))
66 .await?;
67 clients.insert(federation_id, Arc::new(client));
68 }
69
70 Ok(Self {
71 db,
72 clients: Arc::new(RwLock::new(clients)),
73 invoice_generated: Arc::new(Default::default()),
74 base_url,
75 })
76 }
77
78 fn default_secret() -> DerivableSecret {
82 DerivableSecret::new_root(&[], &[])
83 }
84
85 pub async fn register_federation(
86 &self,
87 invite_code: &InviteCode,
88 ) -> Result<FederationId, RecurringPaymentError> {
89 let federation_id = invite_code.federation_id();
90 info!("Registering federation {}", federation_id);
91
92 let mut clients = self.clients.write().await;
95 if clients.contains_key(&federation_id) {
96 return Err(RecurringPaymentError::FederationAlreadyRegistered(
97 federation_id,
98 ));
99 }
100
101 let client_db_prefix = FederationDbPrefix::random();
106 let client_db = open_client_db(&self.db, client_db_prefix);
107
108 match Self::join_federation_static(client_db, invite_code).await {
109 Ok(client) => {
110 try_add_federation_database(&self.db, federation_id, client_db_prefix)
111 .await
112 .expect("We hold a global lock, no parallel joining can happen");
113 clients.insert(federation_id, client);
114 Ok(federation_id)
115 }
116 Err(e) => {
117 Err(e)
119 }
120 }
121 }
122
123 async fn join_federation_static(
124 client_db: Database,
125 invite_code: &InviteCode,
126 ) -> Result<ClientHandleArc, RecurringPaymentError> {
127 let mut client_builder = Client::builder(client_db)
128 .await
129 .map_err(RecurringPaymentError::JoiningFederationFailed)?;
130
131 client_builder.with_connector(Connector::default());
132 client_builder.with_module(LightningClientInit::default());
133 client_builder.with_module(MintClientInit);
134 client_builder.with_primary_module_kind(ModuleKind::from_static_str("mint"));
135
136 let client = client_builder
137 .preview(invite_code)
138 .await?
139 .join(fedimint_client::RootSecret::StandardDoubleDerive(
140 Self::default_secret(),
141 ))
142 .await
143 .map_err(RecurringPaymentError::JoiningFederationFailed)?;
144 Ok(Arc::new(client))
145 }
146
147 pub async fn register_recurring_payment_code(
148 &self,
149 federation_id: FederationId,
150 payment_code_root_key: PaymentCodeRootKey,
151 protocol: RecurringPaymentProtocol,
152 meta: &str,
153 ) -> Result<String, RecurringPaymentError> {
154 if protocol != RecurringPaymentProtocol::LNURL {
156 return Err(RecurringPaymentError::UnsupportedProtocol(protocol));
157 }
158
159 self.get_federation_client(federation_id).await?;
161
162 let payment_code = self.create_lnurl(payment_code_root_key.to_payment_code_id());
163 let payment_code_entry = PaymentCodeEntry {
164 root_key: payment_code_root_key,
165 federation_id,
166 protocol,
167 payment_code: payment_code.clone(),
168 variant: PaymentCodeVariant::Lnurl {
169 meta: meta.to_owned(),
170 },
171 };
172
173 let mut dbtx = self.db.begin_transaction().await;
174 if let Some(existing_code) = dbtx
175 .insert_entry(
176 &PaymentCodeKey {
177 payment_code_id: payment_code_root_key.to_payment_code_id(),
178 },
179 &payment_code_entry,
180 )
181 .await
182 {
183 if existing_code != payment_code_entry {
184 return Err(RecurringPaymentError::PaymentCodeAlreadyExists(
185 payment_code_root_key,
186 ));
187 }
188
189 dbtx.ignore_uncommitted();
190 return Ok(payment_code);
191 }
192
193 dbtx.insert_new_entry(
194 &PaymentCodeNextInvoiceIndexKey {
195 payment_code_id: payment_code_root_key.to_payment_code_id(),
196 },
197 &0,
198 )
199 .await;
200 dbtx.commit_tx_result().await?;
201
202 Ok(payment_code)
203 }
204
205 fn create_lnurl(&self, payment_code_id: PaymentCodeId) -> String {
206 let lnurl = LnUrl::from_url(format!(
207 "{}lnv1/paycodes/{}",
208 self.base_url, payment_code_id
209 ));
210 lnurl.encode()
211 }
212
213 pub async fn lnurl_pay(
214 &self,
215 payment_code_id: PaymentCodeId,
216 ) -> Result<PayResponse, RecurringPaymentError> {
217 let payment_code = self.get_payment_code(payment_code_id).await?;
218 let PaymentCodeVariant::Lnurl { meta } = payment_code.variant;
219
220 Ok(PayResponse {
221 callback: format!("{}lnv1/paycodes/{}/invoice", self.base_url, payment_code_id),
222 max_sendable: 100000000000,
223 min_sendable: 1,
224 tag: Tag::PayRequest,
225 metadata: meta,
226 comment_allowed: None,
227 allows_nostr: None,
228 nostr_pubkey: None,
229 })
230 }
231
232 pub async fn lnurl_invoice(
233 &self,
234 payment_code_id: PaymentCodeId,
235 amount: Amount,
236 ) -> Result<LNURLPayInvoice, RecurringPaymentError> {
237 let (operation_id, federation_id, invoice) =
238 self.create_bolt11_invoice(payment_code_id, amount).await?;
239 Ok(LNURLPayInvoice {
240 pr: invoice.to_string(),
241 verify: format!(
242 "{}lnv1/verify/{}/{}",
243 self.base_url,
244 federation_id,
245 operation_id.fmt_full()
246 ),
247 })
248 }
249
250 async fn create_bolt11_invoice(
251 &self,
252 payment_code_id: PaymentCodeId,
253 amount: Amount,
254 ) -> Result<(OperationId, FederationId, Bolt11Invoice), RecurringPaymentError> {
255 const DEFAULT_EXPIRY_TIME: u64 = 60 * 60 * 24;
258
259 let payment_code = self.get_payment_code(payment_code_id).await?;
260 let invoice_index = self.get_next_invoice_index(payment_code_id).await;
261
262 let federation_client = self
263 .get_federation_client(payment_code.federation_id)
264 .await?;
265 let federation_client_ln_module = federation_client
266 .get_first_module::<LightningClientModule>()
267 .map_err(|e| {
268 warn!("No compatible lightning module found {e}");
269 RecurringPaymentError::NoLightningModuleFound
270 })?;
271
272 let gateway = federation_client_ln_module
273 .get_gateway(None, false)
274 .await?
275 .ok_or(RecurringPaymentError::NoGatewayFound)?;
276
277 let lnurl_meta = match payment_code.variant {
278 PaymentCodeVariant::Lnurl { meta } => meta,
279 };
280 let meta_hash = Sha256(sha256::Hash::hash(lnurl_meta.as_bytes()));
281 let description = Bolt11InvoiceDescription::Hash(meta_hash);
282
283 let (operation_id, invoice, _preimage) = federation_client_ln_module
286 .create_bolt11_invoice_for_user_tweaked(
287 amount,
288 description,
289 Some(DEFAULT_EXPIRY_TIME),
290 payment_code.root_key.0,
291 invoice_index,
292 serde_json::Value::Null,
293 Some(gateway),
294 )
295 .await?;
296
297 let mut dbtx = self.db.begin_transaction().await;
298 dbtx.insert_new_entry(
299 &PaymentCodeInvoiceKey {
300 payment_code_id,
301 index: invoice_index,
302 },
303 &PaymentCodeInvoiceEntry {
304 operation_id,
305 invoice: PaymentCodeInvoice::Bolt11(invoice.clone()),
306 },
307 )
308 .await;
309
310 let invoice_generated_notifier = self.invoice_generated.clone();
311 dbtx.on_commit(move || {
312 invoice_generated_notifier.notify_waiters();
313 });
314 dbtx.commit_tx().await;
315
316 await_invoice_confirmed(&federation_client_ln_module, operation_id).await?;
317
318 Ok((operation_id, federation_client.federation_id(), invoice))
319 }
320
321 async fn get_federation_client(
322 &self,
323 federation_id: FederationId,
324 ) -> Result<ClientHandleArc, RecurringPaymentError> {
325 self.clients
326 .read()
327 .await
328 .get(&federation_id)
329 .cloned()
330 .ok_or(RecurringPaymentError::UnknownFederationId(federation_id))
331 }
332
333 pub async fn await_invoice_index_generated(
334 &self,
335 payment_code_id: PaymentCodeId,
336 invoice_index: u64,
337 ) -> Result<PaymentCodeInvoiceEntry, RecurringPaymentError> {
338 self.get_payment_code(payment_code_id).await?;
339
340 let mut notified = self.invoice_generated.notified();
341 loop {
342 let mut dbtx = self.db.begin_transaction_nc().await;
343 if let Some(invoice_entry) = dbtx
344 .get_value(&PaymentCodeInvoiceKey {
345 payment_code_id,
346 index: invoice_index,
347 })
348 .await
349 {
350 break Ok(invoice_entry);
351 };
352
353 notified.await;
354 notified = self.invoice_generated.notified();
355 }
356 }
357
358 async fn get_next_invoice_index(&self, payment_code_id: PaymentCodeId) -> u64 {
359 self.db
360 .autocommit(
361 |dbtx, _| {
362 Box::pin(async move {
363 let next_index = dbtx
364 .get_value(&PaymentCodeNextInvoiceIndexKey { payment_code_id })
365 .await
366 .map(|index| index + 1)
367 .unwrap_or(0);
368 dbtx.insert_entry(
369 &PaymentCodeNextInvoiceIndexKey { payment_code_id },
370 &next_index,
371 )
372 .await;
373 Result::<_, ()>::Ok(next_index)
374 })
375 },
376 None,
377 )
378 .await
379 .expect("Loops forever and never returns errors internally")
380 }
381
382 pub async fn list_federations(&self) -> Vec<FederationId> {
383 self.clients.read().await.keys().cloned().collect()
384 }
385
386 async fn get_payment_code(
387 &self,
388 payment_code_id: PaymentCodeId,
389 ) -> Result<PaymentCodeEntry, RecurringPaymentError> {
390 self.db
391 .begin_transaction_nc()
392 .await
393 .get_value(&PaymentCodeKey { payment_code_id })
394 .await
395 .ok_or(RecurringPaymentError::UnknownPaymentCode(payment_code_id))
396 }
397
398 pub async fn verify_invoice_paid(
407 &self,
408 federation_id: FederationId,
409 operation_id: OperationId,
410 ) -> Result<InvoiceStatus, RecurringPaymentError> {
411 let federation_client = self.get_federation_client(federation_id).await?;
412
413 let invoice = {
416 let operation = federation_client
417 .operation_log()
418 .get_operation(operation_id)
419 .await
420 .ok_or(RecurringPaymentError::UnknownInvoice(operation_id))?;
421
422 if operation.operation_module_kind() != LightningClientModule::kind().as_str() {
423 return Err(RecurringPaymentError::UnknownInvoice(operation_id));
424 }
425
426 let LightningOperationMetaVariant::Receive { invoice, .. } =
427 operation.meta::<LightningOperationMeta>().variant
428 else {
429 return Err(RecurringPaymentError::UnknownInvoice(operation_id));
430 };
431
432 invoice
433 };
434
435 let ln_module = federation_client
436 .get_first_module::<LightningClientModule>()
437 .map_err(|e| {
438 warn!("No compatible lightning module found {e}");
439 RecurringPaymentError::NoLightningModuleFound
440 })?;
441
442 let mut stream = ln_module
443 .subscribe_ln_receive(operation_id)
444 .await
445 .map_err(|_| RecurringPaymentError::UnknownInvoice(operation_id))?
446 .into_stream();
447 let status = loop {
448 let update = timeout(Duration::from_millis(100), stream.next()).await;
456 match update {
457 Ok(Some(LnReceiveState::Funded | LnReceiveState::Claimed)) => {
461 break PaymentStatus::Paid;
462 }
463 Ok(Some(_)) => {
465 continue;
466 }
467 Ok(None) | Err(_) => {
471 break PaymentStatus::Pending;
472 }
473 }
474 };
475
476 Ok(InvoiceStatus { invoice, status })
477 }
478}
479
480async fn await_invoice_confirmed(
481 ln_module: &ClientModuleInstance<'_, LightningClientModule>,
482 operation_id: OperationId,
483) -> Result<(), RecurringPaymentError> {
484 let mut operation_updated = ln_module
485 .subscribe_ln_receive(operation_id)
486 .await?
487 .into_stream();
488
489 while let Some(update) = operation_updated.next().await {
490 if matches!(update, LnReceiveState::WaitingForPayment { .. }) {
491 return Ok(());
492 }
493 }
494
495 Err(RecurringPaymentError::Other(anyhow!(
496 "BOLT11 invoice not confirmed"
497 )))
498}
499
500#[derive(Debug, Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
501pub enum PaymentCodeInvoice {
502 Bolt11(Bolt11Invoice),
503}
504
505pub struct InvoiceStatus {
508 pub invoice: Bolt11Invoice,
509 pub status: PaymentStatus,
510}
511
512pub enum PaymentStatus {
513 Paid,
514 Pending,
515}
516
517impl PaymentStatus {
518 pub fn is_paid(&self) -> bool {
519 matches!(self, PaymentStatus::Paid)
520 }
521}
522
523#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
527pub struct LNURLPayInvoice {
528 pub pr: String,
529 pub verify: String,
530}