fedimint_recurringd/
lib.rs

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    /// We don't want to hold any money or sign anything ourselves, we only use
69    /// the client with externally supplied key material and to track
70    /// ongoing progress of other users' receives.
71    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        // We lock to prevent parallel join attempts
83        // TODO: lock per federation
84        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        // We don't know if joining will succeed or be interrupted. We use a random DB
92        // prefix to initialize the client and only write the prefix to the DB if that
93        // succeeds. If it fails we end up with some orphaned data in the DB, if it ever
94        // becomes a problem we can clean it up later.
95        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                // TODO: clean up DB?
108                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        // TODO: support BOLT12
146        if protocol != RecurringPaymentProtocol::LNURL {
147            return Err(RecurringPaymentError::UnsupportedProtocol(protocol));
148        }
149
150        // Ensure the federation is supported
151        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        // Invoices are valid for one day by default, might become dynamic with BOLT12
241        // support
242        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        // TODO: ideally creating the invoice would take a dbtx as argument so we don't
269        // get holes in our used indexes in case this function fails/is cancelled
270        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}