fedimint_recurringd/
lib.rs

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    /// We don't want to hold any money or sign anything ourselves, we only use
79    /// the client with externally supplied key material and to track
80    /// ongoing progress of other users' receives.
81    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        // We lock to prevent parallel join attempts
93        // TODO: lock per federation
94        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        // We don't know if joining will succeed or be interrupted. We use a random DB
102        // prefix to initialize the client and only write the prefix to the DB if that
103        // succeeds. If it fails we end up with some orphaned data in the DB, if it ever
104        // becomes a problem we can clean it up later.
105        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                // TODO: clean up DB?
118                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        // TODO: support BOLT12
155        if protocol != RecurringPaymentProtocol::LNURL {
156            return Err(RecurringPaymentError::UnsupportedProtocol(protocol));
157        }
158
159        // Ensure the federation is supported
160        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        // Invoices are valid for one day by default, might become dynamic with BOLT12
256        // support
257        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        // TODO: ideally creating the invoice would take a dbtx as argument so we don't
284        // get holes in our used indexes in case this function fails/is cancelled
285        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    /// Returns if an invoice has been paid yet. To avoid DB indirection and
399    /// since the URLs would be similarly long either way we identify
400    /// invoices by federation id and operation id instead of the payment
401    /// code. This function is the basis of `recurringd`'s [LUD-21]
402    /// implementation that allows clients to verify if a given invoice they
403    /// generated using the LNURL has been paid yet.
404    ///
405    /// [LUD-21]: https://github.com/lnurl/luds/blob/luds/21.md
406    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        // Unfortunately LUD-21 wants us to return the invoice again, so we have to
414        // fetch it from the operation meta.
415        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            // Unfortunately the fedimint client doesn't track payment status internally
449            // yet, but relies on integrators to consume the update streams belonging to
450            // operations to figure out their state. Since the verify endpoint is meant to
451            // be non-blocking, we need to find a way to consume the stream until we think
452            // no immediate progress will be made anymore. That's why we limit each update
453            // step to 100ms, far more than a DB read should ever take, and abort if we'd
454            // block to wait for further progress to be made.
455            let update = timeout(Duration::from_millis(100), stream.next()).await;
456            match update {
457                // For some reason recurringd jumps right to claimed without going over funded … but
458                // either is fine to conclude the user will receive their money once they come
459                // online.
460                Ok(Some(LnReceiveState::Funded | LnReceiveState::Claimed)) => {
461                    break PaymentStatus::Paid;
462                }
463                // Keep looking for a state update indicating the invoice having been paid
464                Ok(Some(_)) => {
465                    continue;
466                }
467                // If we reach the end of the update stream without observing a state indicating the
468                // invoice having been paid there was likely some error or the invoice timed out.
469                // Either way we just show the invoice as unpaid.
470                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
505/// Helper struct indicating if an invoice was paid. In the future it may also
506/// contain the preimage to be fully LUD-21 compliant.
507pub 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/// The lnurl-rs crate doesn't have the `verify` field in this type and we don't
524/// use any of the other fields right now. Once we upstream the verify field
525/// this struct can be removed.
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
527pub struct LNURLPayInvoice {
528    pub pr: String,
529    pub verify: String,
530}