Skip to main content

fedimint_recurringd/
lib.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::anyhow;
6use fedimint_client::meta::MetaService;
7use fedimint_client::{Client, ClientHandleArc, ClientModule, ClientModuleInstance};
8use fedimint_client_module::meta::LegacyMetaSource;
9use fedimint_connectors::ConnectorRegistry;
10use fedimint_core::config::FederationId;
11use fedimint_core::core::OperationId;
12use fedimint_core::db::{
13    AutocommitResultExt, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
14    IRawDatabase,
15};
16use fedimint_core::encoding::{Decodable, Encodable};
17use fedimint_core::invite_code::InviteCode;
18use fedimint_core::secp256k1::hashes::sha256;
19use fedimint_core::secp256k1::{PublicKey, SECP256K1};
20use fedimint_core::task::timeout;
21use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl};
22use fedimint_core::{Amount, BitcoinHash, runtime};
23use fedimint_derive_secret::DerivableSecret;
24use fedimint_ln_client::common::{LightningGateway, LightningGatewayAnnouncement};
25use fedimint_ln_client::recurring::{
26    PaymentCodeId, PaymentCodeRootKey, RecurringPaymentError, RecurringPaymentProtocol,
27};
28use fedimint_ln_client::{
29    LightningClientInit, LightningClientModule, LightningOperationMeta,
30    LightningOperationMetaVariant, LnReceiveState, tweak_user_key,
31};
32use fedimint_lnurl::{PayResponse, encode_lnurl, pay_request_tag};
33use fedimint_meta_client::MetaModuleMetaSourceWithFallback;
34use fedimint_mint_client::MintClientInit;
35use futures::StreamExt;
36use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Sha256};
37use serde::{Deserialize, Serialize};
38use tokio::sync::{Notify, RwLock, watch};
39use tracing::{debug, info, warn};
40
41use crate::db::{
42    FederationDbPrefix, PaymentCodeEntry, PaymentCodeInvoiceEntry, PaymentCodeInvoiceKey,
43    PaymentCodeKey, PaymentCodeNextInvoiceIndexKey, PaymentCodeVariant, SchemaVersionKey,
44    load_federation_client_databases, open_client_db, try_add_federation_database,
45};
46
47mod db;
48
49#[derive(Clone)]
50pub struct RecurringInvoiceServer {
51    db: Database,
52    connectors: ConnectorRegistry,
53    clients: Arc<RwLock<HashMap<FederationId, ClientHandleArc>>>,
54    gateway_cache: Arc<RwLock<HashMap<FederationId, watch::Receiver<Vec<CachedGateway>>>>>,
55    invoice_generated: Arc<Notify>,
56    base_url: SafeUrl,
57}
58
59#[derive(Clone)]
60struct CachedGateway {
61    gateway: LightningGateway,
62    vetted: bool,
63}
64
65impl RecurringInvoiceServer {
66    pub async fn new(
67        connectors: ConnectorRegistry,
68        db: impl IRawDatabase + 'static,
69        base_url: SafeUrl,
70    ) -> anyhow::Result<Self> {
71        let db = Database::new(db, Default::default());
72
73        let mut clients = HashMap::<_, ClientHandleArc>::new();
74        let mut gateway_cache = HashMap::<FederationId, watch::Receiver<Vec<CachedGateway>>>::new();
75
76        for (federation_id, db) in load_federation_client_databases(&db).await {
77            let mut client_builder = Client::builder().await;
78            client_builder.with_meta_service(recurringd_meta_service());
79            client_builder.with_module(LightningClientInit::default());
80            client_builder.with_module(MintClientInit);
81            let client = client_builder
82                .open(
83                    connectors.clone(),
84                    db,
85                    fedimint_client::RootSecret::StandardDoubleDerive(Self::default_secret()),
86                )
87                .await?;
88            let client = Arc::new(client);
89            gateway_cache.insert(
90                federation_id,
91                spawn_gateway_cache_refresh(federation_id, &client),
92            );
93            clients.insert(federation_id, client);
94        }
95
96        let slf = Self {
97            db: db.clone(),
98            clients: Arc::new(RwLock::new(clients)),
99            gateway_cache: Arc::new(RwLock::new(gateway_cache)),
100            invoice_generated: Arc::new(Default::default()),
101            base_url,
102            connectors,
103        };
104
105        slf.run_db_migrations().await;
106
107        Ok(slf)
108    }
109
110    /// We don't want to hold any money or sign anything ourselves, we only use
111    /// the client with externally supplied key material and to track
112    /// ongoing progress of other users' receives.
113    fn default_secret() -> DerivableSecret {
114        DerivableSecret::new_root(&[], &[])
115    }
116
117    pub async fn register_federation(
118        &self,
119        invite_code: &InviteCode,
120    ) -> Result<FederationId, RecurringPaymentError> {
121        let federation_id = invite_code.federation_id();
122        info!("Registering federation {}", federation_id);
123
124        // We lock to prevent parallel join attempts
125        // TODO: lock per federation
126        let mut clients = self.clients.write().await;
127        if clients.contains_key(&federation_id) {
128            return Err(RecurringPaymentError::FederationAlreadyRegistered(
129                federation_id,
130            ));
131        }
132
133        // We don't know if joining will succeed or be interrupted. We use a random DB
134        // prefix to initialize the client and only write the prefix to the DB if that
135        // succeeds. If it fails we end up with some orphaned data in the DB, if it ever
136        // becomes a problem we can clean it up later.
137        let client_db_prefix = FederationDbPrefix::random();
138        let client_db = open_client_db(&self.db, client_db_prefix);
139
140        match Self::join_federation_static(self.connectors.clone(), client_db, invite_code).await {
141            Ok(client) => {
142                try_add_federation_database(&self.db, federation_id, client_db_prefix)
143                    .await
144                    .expect("We hold a global lock, no parallel joining can happen");
145                self.gateway_cache.write().await.insert(
146                    federation_id,
147                    spawn_gateway_cache_refresh(federation_id, &client),
148                );
149                clients.insert(federation_id, client);
150                Ok(federation_id)
151            }
152            Err(e) => {
153                // TODO: clean up DB?
154                Err(e)
155            }
156        }
157    }
158
159    async fn join_federation_static(
160        connectors: ConnectorRegistry,
161        client_db: Database,
162        invite_code: &InviteCode,
163    ) -> Result<ClientHandleArc, RecurringPaymentError> {
164        let mut client_builder = Client::builder().await;
165
166        client_builder.with_meta_service(recurringd_meta_service());
167        client_builder.with_module(LightningClientInit::default());
168        client_builder.with_module(MintClientInit);
169
170        let client = client_builder
171            .preview(connectors, invite_code)
172            .await
173            .map_err(|err| {
174                RecurringPaymentError::JoiningFederationFailed(anyhow!("{}", err.fmt_compact()))
175            })?
176            .join(
177                client_db,
178                fedimint_client::RootSecret::StandardDoubleDerive(Self::default_secret()),
179            )
180            .await
181            .map_err(|err| {
182                RecurringPaymentError::JoiningFederationFailed(anyhow!("{}", err.fmt_compact()))
183            })?;
184        Ok(Arc::new(client))
185    }
186
187    pub async fn register_recurring_payment_code(
188        &self,
189        federation_id: FederationId,
190        payment_code_root_key: PaymentCodeRootKey,
191        protocol: RecurringPaymentProtocol,
192        meta: &str,
193    ) -> Result<String, RecurringPaymentError> {
194        // TODO: support BOLT12
195        if protocol != RecurringPaymentProtocol::LNURL {
196            return Err(RecurringPaymentError::UnsupportedProtocol(protocol));
197        }
198
199        // Ensure the federation is supported
200        self.get_federation_client(federation_id).await?;
201
202        let payment_code = self.create_lnurl(payment_code_root_key.to_payment_code_id());
203        let payment_code_entry = PaymentCodeEntry {
204            root_key: payment_code_root_key,
205            federation_id,
206            protocol,
207            payment_code: payment_code.clone(),
208            variant: PaymentCodeVariant::Lnurl {
209                meta: meta.to_owned(),
210            },
211        };
212
213        let mut dbtx = self.db.begin_transaction().await;
214        if let Some(existing_code) = dbtx
215            .insert_entry(
216                &PaymentCodeKey {
217                    payment_code_id: payment_code_root_key.to_payment_code_id(),
218                },
219                &payment_code_entry,
220            )
221            .await
222        {
223            if existing_code != payment_code_entry {
224                return Err(RecurringPaymentError::PaymentCodeAlreadyExists(
225                    payment_code_root_key,
226                ));
227            }
228
229            dbtx.ignore_uncommitted();
230            return Ok(payment_code);
231        }
232
233        dbtx.insert_new_entry(
234            &PaymentCodeNextInvoiceIndexKey {
235                payment_code_id: payment_code_root_key.to_payment_code_id(),
236            },
237            &0,
238        )
239        .await;
240        dbtx.commit_tx_result().await.map_err(anyhow::Error::from)?;
241
242        Ok(payment_code)
243    }
244
245    fn create_lnurl(&self, payment_code_id: PaymentCodeId) -> String {
246        encode_lnurl(
247            &self
248                .base_url
249                .join_path(&format!("lnv1/paycodes/{payment_code_id}"))
250                .to_string(),
251        )
252    }
253
254    pub async fn lnurl_pay(
255        &self,
256        payment_code_id: PaymentCodeId,
257    ) -> Result<PayResponse, RecurringPaymentError> {
258        let payment_code = self.get_payment_code(payment_code_id).await?;
259        let PaymentCodeVariant::Lnurl { meta } = payment_code.variant;
260
261        Ok(PayResponse {
262            callback: self
263                .base_url
264                .join_path(&format!("lnv1/paycodes/{payment_code_id}/invoice"))
265                .to_string(),
266            max_sendable: 100000000000,
267            min_sendable: 1,
268            tag: pay_request_tag(),
269            metadata: meta,
270        })
271    }
272
273    pub async fn lnurl_invoice(
274        &self,
275        payment_code_id: PaymentCodeId,
276        amount: Amount,
277    ) -> Result<LNURLPayInvoice, RecurringPaymentError> {
278        let (operation_id, federation_id, invoice) =
279            self.create_bolt11_invoice(payment_code_id, amount).await?;
280        Ok(LNURLPayInvoice {
281            pr: invoice.to_string(),
282            verify: self
283                .base_url
284                .join_path(&format!(
285                    "lnv1/verify/{federation_id}/{}",
286                    operation_id.fmt_full()
287                ))
288                .to_string(),
289        })
290    }
291
292    async fn create_bolt11_invoice(
293        &self,
294        payment_code_id: PaymentCodeId,
295        amount: Amount,
296    ) -> Result<(OperationId, FederationId, Bolt11Invoice), RecurringPaymentError> {
297        // Invoices are valid for one day by default, might become dynamic with BOLT12
298        // support
299        const DEFAULT_EXPIRY_TIME: u64 = 60 * 60 * 24;
300
301        let payment_code = self.get_payment_code(payment_code_id).await?;
302
303        let federation_client = self
304            .get_federation_client(payment_code.federation_id)
305            .await?;
306
307        let gateway = self
308            .get_cached_gateway(payment_code.federation_id, amount)
309            .await?;
310
311        let (operation_id, invoice) = self
312            .db
313            .autocommit(
314                |dbtx, _| {
315                    let federation_client = federation_client.clone();
316                    let payment_code = payment_code.clone();
317                    let gateway = gateway.clone();
318                    Box::pin(async move {
319                        let mut invoice_index = self
320                            .get_next_invoice_index(&mut dbtx.to_ref_nc(), payment_code_id)
321                            .await;
322
323                        // Check if any invoice indices were already used in aborted calls to this
324                        // fn. If so:
325                        //   1. Save each previously generated invoice. We don't want to reuse it
326                        //      since it may be expired and in the future may contain call-specific
327                        //      data, but also want to allow the client to sync past it.
328                        //   2. Increment the invoice index until we find an unused one, since
329                        //      re-using an index would re-use an operation id, which is forbidden.
330                        //
331                        // A single request can only create one orphaned operation, but multiple
332                        // cancelled/restarted requests in a row can leave multiple consecutive
333                        // orphaned operations before recurringd commits its own DB state.
334                        let invoice_index = loop {
335                            let operation_id =
336                                operation_id_from_user_key(payment_code.root_key, invoice_index);
337
338                            let Some(invoice) =
339                                Self::check_if_invoice_exists(&federation_client, operation_id)
340                                    .await
341                            else {
342                                break invoice_index;
343                            };
344
345                            self.save_bolt11_invoice(
346                                dbtx,
347                                operation_id,
348                                payment_code_id,
349                                invoice_index,
350                                invoice,
351                            )
352                            .await;
353
354                            invoice_index = self
355                                .get_next_invoice_index(&mut dbtx.to_ref_nc(), payment_code_id)
356                                .await;
357                        };
358
359                        // This is where the main part starts: generate the invoice and save it to
360                        // the DB
361                        let federation_client_ln_module = federation_client.get_ln_module()?;
362
363                        let lnurl_meta = match payment_code.variant {
364                            PaymentCodeVariant::Lnurl { meta } => meta,
365                        };
366                        let meta_hash = Sha256(sha256::Hash::hash(lnurl_meta.as_bytes()));
367                        let description = Bolt11InvoiceDescription::Hash(meta_hash);
368
369                        // TODO: ideally creating the invoice would take a dbtx as argument so we
370                        // don't have to do the "check if invoice already exists" dance
371                        let (operation_id, invoice, _preimage) = federation_client_ln_module
372                            .create_bolt11_invoice_for_user_tweaked(
373                                amount,
374                                description,
375                                Some(DEFAULT_EXPIRY_TIME),
376                                payment_code.root_key.0,
377                                invoice_index,
378                                serde_json::Value::Null,
379                                Some(gateway),
380                            )
381                            .await?;
382
383                        self.save_bolt11_invoice(
384                            dbtx,
385                            operation_id,
386                            payment_code_id,
387                            invoice_index,
388                            invoice.clone(),
389                        )
390                        .await;
391
392                        Result::<_, anyhow::Error>::Ok((operation_id, invoice))
393                    })
394                },
395                None,
396            )
397            .await
398            .unwrap_autocommit()?;
399
400        await_invoice_confirmed(&federation_client.get_ln_module()?, operation_id).await?;
401
402        Ok((operation_id, federation_client.federation_id(), invoice))
403    }
404
405    async fn save_bolt11_invoice(
406        &self,
407        dbtx: &mut DatabaseTransaction<'_>,
408        operation_id: OperationId,
409        payment_code_id: PaymentCodeId,
410        invoice_index: u64,
411        invoice: Bolt11Invoice,
412    ) {
413        dbtx.insert_new_entry(
414            &PaymentCodeInvoiceKey {
415                payment_code_id,
416                index: invoice_index,
417            },
418            &PaymentCodeInvoiceEntry {
419                operation_id,
420                invoice: PaymentCodeInvoice::Bolt11(invoice.clone()),
421            },
422        )
423        .await;
424
425        let invoice_generated_notifier = self.invoice_generated.clone();
426        dbtx.on_commit(move || {
427            invoice_generated_notifier.notify_waiters();
428        });
429    }
430
431    async fn check_if_invoice_exists(
432        federation_client: &ClientHandleArc,
433        operation_id: OperationId,
434    ) -> Option<Bolt11Invoice> {
435        let operation = federation_client
436            .operation_log()
437            .get_operation(operation_id)
438            .await?;
439
440        assert_eq!(
441            operation.operation_module_kind(),
442            LightningClientModule::kind().as_str()
443        );
444
445        let LightningOperationMetaVariant::Receive { invoice, .. } =
446            operation.meta::<LightningOperationMeta>().variant
447        else {
448            panic!(
449                "Unexpected operation meta variant: {:?}",
450                operation.meta::<LightningOperationMeta>().variant
451            );
452        };
453
454        Some(invoice)
455    }
456
457    async fn get_federation_client(
458        &self,
459        federation_id: FederationId,
460    ) -> Result<ClientHandleArc, RecurringPaymentError> {
461        self.clients
462            .read()
463            .await
464            .get(&federation_id)
465            .cloned()
466            .ok_or(RecurringPaymentError::UnknownFederationId(federation_id))
467    }
468
469    async fn get_cached_gateway(
470        &self,
471        federation_id: FederationId,
472        amount: Amount,
473    ) -> Result<LightningGateway, RecurringPaymentError> {
474        const EMPTY_GATEWAY_CACHE_WAIT: Duration = Duration::from_secs(60);
475
476        let mut gateway_cache = self
477            .gateway_cache
478            .read()
479            .await
480            .get(&federation_id)
481            .cloned()
482            .ok_or(RecurringPaymentError::NoGatewayFound)?;
483
484        if let Some(gateway) = select_preferred_gateway(&gateway_cache.borrow(), amount) {
485            return Ok(gateway);
486        }
487
488        timeout(EMPTY_GATEWAY_CACHE_WAIT, async {
489            loop {
490                gateway_cache
491                    .changed()
492                    .await
493                    .map_err(|_| RecurringPaymentError::NoGatewayFound)?;
494
495                if let Some(gateway) =
496                    select_preferred_gateway(&gateway_cache.borrow_and_update(), amount)
497                {
498                    break Ok(gateway);
499                }
500            }
501        })
502        .await
503        .map_err(|_| RecurringPaymentError::NoGatewayFound)?
504    }
505
506    pub async fn await_invoice_index_generated(
507        &self,
508        payment_code_id: PaymentCodeId,
509        invoice_index: u64,
510    ) -> Result<PaymentCodeInvoiceEntry, RecurringPaymentError> {
511        self.get_payment_code(payment_code_id).await?;
512
513        let mut notified = self.invoice_generated.notified();
514        loop {
515            let mut dbtx = self.db.begin_transaction_nc().await;
516            if let Some(invoice_entry) = dbtx
517                .get_value(&PaymentCodeInvoiceKey {
518                    payment_code_id,
519                    index: invoice_index,
520                })
521                .await
522            {
523                break Ok(invoice_entry);
524            };
525
526            notified.await;
527            notified = self.invoice_generated.notified();
528        }
529    }
530
531    async fn get_next_invoice_index(
532        &self,
533        dbtx: &mut DatabaseTransaction<'_>,
534        payment_code_id: PaymentCodeId,
535    ) -> u64 {
536        let next_index = dbtx
537            .get_value(&PaymentCodeNextInvoiceIndexKey { payment_code_id })
538            .await
539            .map(|index| index + 1)
540            .unwrap_or(0);
541        dbtx.insert_entry(
542            &PaymentCodeNextInvoiceIndexKey { payment_code_id },
543            &next_index,
544        )
545        .await;
546
547        next_index
548    }
549
550    pub async fn list_federations(&self) -> Vec<FederationId> {
551        self.clients.read().await.keys().cloned().collect()
552    }
553
554    async fn get_payment_code(
555        &self,
556        payment_code_id: PaymentCodeId,
557    ) -> Result<PaymentCodeEntry, RecurringPaymentError> {
558        self.db
559            .begin_transaction_nc()
560            .await
561            .get_value(&PaymentCodeKey { payment_code_id })
562            .await
563            .ok_or(RecurringPaymentError::UnknownPaymentCode(payment_code_id))
564    }
565
566    /// Returns if an invoice has been paid yet. To avoid DB indirection and
567    /// since the URLs would be similarly long either way we identify
568    /// invoices by federation id and operation id instead of the payment
569    /// code. This function is the basis of `recurringd`'s [LUD-21]
570    /// implementation that allows clients to verify if a given invoice they
571    /// generated using the LNURL has been paid yet.
572    ///
573    /// [LUD-21]: https://github.com/lnurl/luds/blob/luds/21.md
574    pub async fn verify_invoice_paid(
575        &self,
576        federation_id: FederationId,
577        operation_id: OperationId,
578    ) -> Result<InvoiceStatus, RecurringPaymentError> {
579        let federation_client = self.get_federation_client(federation_id).await?;
580
581        // Unfortunately LUD-21 wants us to return the invoice again, so we have to
582        // fetch it from the operation meta.
583        let invoice = {
584            let operation = federation_client
585                .operation_log()
586                .get_operation(operation_id)
587                .await
588                .ok_or(RecurringPaymentError::UnknownInvoice(operation_id))?;
589
590            if operation.operation_module_kind() != LightningClientModule::kind().as_str() {
591                return Err(RecurringPaymentError::UnknownInvoice(operation_id));
592            }
593
594            let LightningOperationMetaVariant::Receive { invoice, .. } =
595                operation.meta::<LightningOperationMeta>().variant
596            else {
597                return Err(RecurringPaymentError::UnknownInvoice(operation_id));
598            };
599
600            invoice
601        };
602
603        let ln_module = federation_client
604            .get_first_module::<LightningClientModule>()
605            .map_err(|e| {
606                warn!("No compatible lightning module found {e}");
607                RecurringPaymentError::NoLightningModuleFound
608            })?;
609
610        let mut stream = ln_module
611            .subscribe_ln_receive(operation_id)
612            .await
613            .map_err(|_| RecurringPaymentError::UnknownInvoice(operation_id))?
614            .into_stream();
615        let status = loop {
616            // Unfortunately the fedimint client doesn't track payment status internally
617            // yet, but relies on integrators to consume the update streams belonging to
618            // operations to figure out their state. Since the verify endpoint is meant to
619            // be non-blocking, we need to find a way to consume the stream until we think
620            // no immediate progress will be made anymore. That's why we limit each update
621            // step to 100ms, far more than a DB read should ever take, and abort if we'd
622            // block to wait for further progress to be made.
623            let update = timeout(Duration::from_millis(100), stream.next()).await;
624            match update {
625                // For some reason recurringd jumps right to claimed without going over funded … but
626                // either is fine to conclude the user will receive their money once they come
627                // online.
628                Ok(Some(LnReceiveState::Funded | LnReceiveState::Claimed)) => {
629                    break PaymentStatus::Paid;
630                }
631                // Keep looking for a state update indicating the invoice having been paid
632                Ok(Some(_)) => {
633                    continue;
634                }
635                // If we reach the end of the update stream without observing a state indicating the
636                // invoice having been paid there was likely some error or the invoice timed out.
637                // Either way we just show the invoice as unpaid.
638                Ok(None) | Err(_) => {
639                    break PaymentStatus::Pending;
640                }
641            }
642        };
643
644        Ok(InvoiceStatus { invoice, status })
645    }
646
647    async fn run_db_migrations(&self) {
648        let migrations = Self::migrations();
649        let schema_version: u64 = self
650            .db
651            .begin_transaction_nc()
652            .await
653            .get_value(&SchemaVersionKey)
654            .await
655            .unwrap_or_default();
656
657        for (target_schema, migration_fn) in migrations
658            .into_iter()
659            .skip_while(|(target_schema, _)| *target_schema <= schema_version)
660        {
661            let mut dbtx = self.db.begin_transaction().await;
662            dbtx.insert_entry(&SchemaVersionKey, &target_schema).await;
663
664            migration_fn(self, dbtx.to_ref_nc()).await;
665
666            dbtx.commit_tx().await;
667        }
668    }
669}
670
671async fn await_invoice_confirmed(
672    ln_module: &ClientModuleInstance<'_, LightningClientModule>,
673    operation_id: OperationId,
674) -> Result<(), RecurringPaymentError> {
675    let mut operation_updated = ln_module
676        .subscribe_ln_receive(operation_id)
677        .await?
678        .into_stream();
679
680    while let Some(update) = operation_updated.next().await {
681        if matches!(update, LnReceiveState::WaitingForPayment { .. }) {
682            return Ok(());
683        }
684    }
685
686    Err(RecurringPaymentError::Other(anyhow!(
687        "BOLT11 invoice not confirmed"
688    )))
689}
690
691#[derive(Debug, Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
692pub enum PaymentCodeInvoice {
693    Bolt11(Bolt11Invoice),
694}
695
696/// Helper struct indicating if an invoice was paid. In the future it may also
697/// contain the preimage to be fully LUD-21 compliant.
698pub struct InvoiceStatus {
699    pub invoice: Bolt11Invoice,
700    pub status: PaymentStatus,
701}
702
703pub enum PaymentStatus {
704    Paid,
705    Pending,
706}
707
708impl PaymentStatus {
709    pub fn is_paid(&self) -> bool {
710        matches!(self, PaymentStatus::Paid)
711    }
712}
713
714/// The lnurl-rs crate doesn't have the `verify` field in this type and we don't
715/// use any of the other fields right now. Once we upstream the verify field
716/// this struct can be removed.
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718pub struct LNURLPayInvoice {
719    pub pr: String,
720    pub verify: String,
721}
722
723fn operation_id_from_user_key(user_key: PaymentCodeRootKey, index: u64) -> OperationId {
724    let invoice_key = tweak_user_key(SECP256K1, user_key.0, index);
725    let preimage = sha256::Hash::hash(&invoice_key.serialize()[..]);
726    let payment_hash = sha256::Hash::hash(&preimage[..]);
727
728    OperationId(payment_hash.to_byte_array())
729}
730
731trait LnClientContextExt {
732    fn get_ln_module(
733        &'_ self,
734    ) -> Result<ClientModuleInstance<'_, LightningClientModule>, RecurringPaymentError>;
735}
736
737impl LnClientContextExt for ClientHandleArc {
738    fn get_ln_module(
739        &'_ self,
740    ) -> Result<ClientModuleInstance<'_, LightningClientModule>, RecurringPaymentError> {
741        self.get_first_module::<LightningClientModule>()
742            .map_err(|e| {
743                warn!("No compatible lightning module found {e}");
744                RecurringPaymentError::NoLightningModuleFound
745            })
746    }
747}
748
749fn recurringd_meta_service() -> Arc<MetaService> {
750    MetaService::new(MetaModuleMetaSourceWithFallback::<LegacyMetaSource>::default())
751}
752
753fn spawn_gateway_cache_refresh(
754    federation_id: FederationId,
755    client: &ClientHandleArc,
756) -> watch::Receiver<Vec<CachedGateway>> {
757    const REFRESH_INTERVAL: Duration = Duration::from_secs(10);
758
759    let (gateway_cache_sender, gateway_cache_receiver) = watch::channel(Vec::new());
760    let task_group = client.task_group().clone();
761    let client = client.clone();
762    task_group.spawn_cancellable("recurringd-gateway-cache-refresh", async move {
763        loop {
764            match select_available_gateways(&client).await {
765                Ok(gateways) => {
766                    gateway_cache_sender.send_replace(gateways);
767                }
768                Err(err) => {
769                    warn!(
770                        federation_id = %federation_id,
771                        err = %err.fmt_compact(),
772                        "Failed to refresh recurringd gateway cache"
773                    );
774                }
775            }
776
777            runtime::sleep(REFRESH_INTERVAL).await;
778        }
779    });
780
781    gateway_cache_receiver
782}
783
784async fn select_available_gateways(
785    client: &ClientHandleArc,
786) -> Result<Vec<CachedGateway>, RecurringPaymentError> {
787    let ln_module = client.get_ln_module()?;
788    ln_module.update_gateway_cache().await.map_err(|err| {
789        warn!(
790            err = %err.fmt_compact_anyhow(),
791            "Failed to refresh gateway announcements"
792        );
793        RecurringPaymentError::NoGatewayFound
794    })?;
795
796    let mut gateways = ln_module.list_gateways().await;
797    if gateways.is_empty() {
798        return Err(RecurringPaymentError::NoGatewayFound);
799    }
800
801    let vetted_gateway_ids = fetch_vetted_gateway_ids(client).await;
802    sort_gateways_by_preference(&mut gateways, &vetted_gateway_ids);
803
804    let mut available_gateways = Vec::new();
805    for gateway in gateways {
806        let gateway_id = gateway.info.gateway_id;
807        let vetted = gateway.vetted || vetted_gateway_ids.contains(&gateway_id);
808        match ln_module
809            .select_available_gateway(Some(gateway.info), None)
810            .await
811        {
812            Ok(gateway) => available_gateways.push(CachedGateway { gateway, vetted }),
813            Err(err) => {
814                debug!(
815                    gateway_id = %gateway_id,
816                    err = %err.fmt_compact_anyhow(),
817                    "Gateway failed availability check"
818                );
819            }
820        }
821    }
822
823    if available_gateways.is_empty() {
824        return Err(RecurringPaymentError::NoGatewayFound);
825    }
826
827    Ok(available_gateways)
828}
829
830fn select_preferred_gateway(
831    gateways: &[CachedGateway],
832    amount: Amount,
833) -> Option<LightningGateway> {
834    gateways
835        .iter()
836        .min_by_key(|gateway| {
837            (
838                !gateway.vetted,
839                gateway_fee_msat(&gateway.gateway, amount),
840                gateway.gateway.gateway_id.serialize(),
841            )
842        })
843        .map(|gateway| gateway.gateway.clone())
844}
845
846fn gateway_fee_msat(gateway: &LightningGateway, amount: Amount) -> u64 {
847    let proportional_fee =
848        (u128::from(amount.msats) * u128::from(gateway.fees.proportional_millionths)) / 1_000_000;
849
850    u64::from(gateway.fees.base_msat)
851        .saturating_add(u64::try_from(proportional_fee).unwrap_or(u64::MAX))
852}
853
854fn sort_gateways_by_preference(
855    gateways: &mut [LightningGatewayAnnouncement],
856    vetted_gateway_ids: &HashSet<PublicKey>,
857) {
858    gateways.sort_by_cached_key(|gateway| {
859        let vetted = gateway.vetted || vetted_gateway_ids.contains(&gateway.info.gateway_id);
860        (
861            !vetted,
862            u64::from(gateway.info.fees.base_msat),
863            gateway.info.gateway_id.serialize(),
864        )
865    });
866}
867
868async fn fetch_vetted_gateway_ids(client: &ClientHandleArc) -> HashSet<PublicKey> {
869    let Some(vetted_gateways) = client
870        .meta_service()
871        .entries(client.db())
872        .await
873        .and_then(|entries| entries.get("vetted_gateways").cloned())
874        .and_then(|value| parse_vetted_gateway_ids(&value))
875    else {
876        debug!("No vetted gateways configured in federation metadata");
877        return HashSet::new();
878    };
879
880    vetted_gateways
881        .into_iter()
882        .filter_map(|gateway_id| match gateway_id.parse::<PublicKey>() {
883            Ok(gateway_id) => Some(gateway_id),
884            Err(err) => {
885                warn!(
886                    %gateway_id,
887                    err = %err.fmt_compact(),
888                    "Failed to parse vetted gateway ID"
889                );
890                None
891            }
892        })
893        .collect()
894}
895
896fn parse_vetted_gateway_ids(value: &serde_json::Value) -> Option<Vec<String>> {
897    if let Ok(gateway_ids) = serde_json::from_value::<Vec<String>>(value.clone()) {
898        return Some(gateway_ids);
899    }
900
901    let value = value.as_str()?;
902
903    // The canonical metadata format is a JSON array of gateway ID strings. Older
904    // configs may have stored that JSON array as a string.
905    match serde_json::from_str::<Vec<String>>(value) {
906        Ok(gateway_ids) => {
907            warn!("vetted_gateways metadata should be configured as a JSON array, not a string");
908            Some(gateway_ids)
909        }
910        Err(_) => None,
911    }
912}